Last active
May 30, 2025 01:36
-
-
Save kseikyo/a3be0194eab64f0f0253a670f95d0961 to your computer and use it in GitHub Desktop.
Migrate from shadcn ui HSL custom config to OKLCH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// file created to migrate from old shadcn hsl config to tailwind v4 | |
// scripts/convert-colors-to-oklch.js | |
import fs from 'fs'; | |
import convert from 'color-convert'; | |
import path from 'path'; | |
// Paths are relative to where the script is run (process.cwd()) | |
const inputFilePathArg = process.argv[2]; | |
const outputFilePathArg = process.argv[3]; | |
if (!inputFilePathArg || !outputFilePathArg) { | |
console.error('Usage: node scripts/convert-colors-to-oklch.js <inputFile> <outputFile>'); | |
console.error('Example: node scripts/convert-colors-to-oklch.js src/styles/input.css src/styles/output.css'); | |
process.exit(1); | |
} | |
const absoluteInputPath = path.resolve(inputFilePathArg); | |
const absoluteOutputPath = path.resolve(outputFilePathArg); | |
try { | |
const cssContent = fs.readFileSync(absoluteInputPath, 'utf8'); | |
const lines = cssContent.split('\n'); | |
const newLines = []; | |
// Regex for HSL-like values: --name: H S% L% [/ A%]; /* comment */ | |
// Example: --black: 230 40% 3%; /* #05060a */ | |
const hslRegex = /^(\s*--[a-zA-Z0-9-]+:\s*)(\d{1,3})\s+(\d{1,3})%\s+(\d{1,3})%(?:\s*\/\s*([\d.]+%))?(\s*;.*)$/; | |
// Regex for HEX values: --name: #RGB or #RRGGBB or #RGBA or #RRGGBBAA; /* comment */ | |
// Example: --blue-900-hex: #2775ca; | |
const hexRegex = /^(\s*--[a-zA-Z0-9-]+:\s*)(#[0-9a-fA-F]{3,8})(\s*;.*)$/; | |
for (const line of lines) { | |
let newLine = line; | |
const hslMatch = line.match(hslRegex); | |
const hexMatch = line.match(hexRegex); | |
if (hslMatch) { | |
const prefix = hslMatch[1]; | |
const h = parseInt(hslMatch[2], 10); | |
const s = parseInt(hslMatch[3], 10); | |
const l = parseInt(hslMatch[4], 10); | |
const alphaPercentString = hslMatch[5]; // e.g., "50%" or undefined | |
try { | |
let convertedColor, colorModelName; | |
if (convert.hsl.oklch) { | |
convertedColor = convert.hsl.oklch([h, s, l]); | |
colorModelName = 'oklch'; | |
} else if (convert.hsl.lch) { | |
console.warn("Oklch direct conversion not found for HSL, falling back to LCH for line: " + line); | |
convertedColor = convert.hsl.lch([h, s, l]); | |
colorModelName = 'lch'; | |
} else { | |
throw new Error('Neither Oklch nor LCH conversion available for HSL.'); | |
} | |
// convertedColor = [L, C, H] | |
// L: 0-100, C: 0-~130 (LCH) or 0-~0.4 (Oklch CSS target), H: 0-360 | |
// CSS: oklch(L% C H [/ A]) or lch(L% C H [/ A]) | |
const lCss = convertedColor[0].toFixed(2); | |
// For Oklch, C is typically 0 to ~0.4. color-convert's oklch C scale is 0-32. | |
// For LCH, C is typically 0 to ~130+. color-convert's lch C scale is 0-133. | |
// We'll scale Oklch's C by /100 for CSS. LCH's C can be used more directly. | |
const cCss = colorModelName === 'oklch' ? (convertedColor[1] / 100).toFixed(4) : convertedColor[1].toFixed(2); | |
const hCss = convertedColor[2].toFixed(2); | |
let colorString = `${colorModelName}(${lCss}% ${cCss} ${hCss}`; | |
if (alphaPercentString) { | |
colorString += ` / ${alphaPercentString}`; | |
} | |
colorString += `)`; | |
const suffix = hslMatch[6]; | |
newLine = `${prefix}${colorString}${suffix}`; | |
} catch (e) { | |
console.warn(`Could not convert HSL value on line: ${line}. Error: ${e.message}`); | |
} | |
} else if (hexMatch) { | |
const prefix = hexMatch[1]; | |
const hexColorWithHash = hexMatch[2]; // e.g., "#2775ca" | |
const suffix = hexMatch[3]; | |
try { | |
const hexValue = hexColorWithHash.substring(1); // Remove # | |
// Determine if there's an alpha component in the hex string | |
let rgb, alphaFromHex = null; | |
if (hexValue.length === 4 || hexValue.length === 8) { // #RGBA or #RRGGBBAA | |
const alphaHex = hexValue.length === 4 ? hexValue.substring(3,4).repeat(2) : hexValue.substring(6,8); | |
alphaFromHex = parseInt(alphaHex, 16) / 255; | |
const colorHex = hexValue.length === 4 ? hexValue.substring(0,3) : hexValue.substring(0,6); | |
rgb = convert.hex.rgb(colorHex); | |
} else { // #RGB or #RRGGBB | |
rgb = convert.hex.rgb(hexValue); | |
} | |
let convertedColor, colorModelName; | |
if (convert.rgb.oklch) { | |
convertedColor = convert.rgb.oklch(rgb); | |
colorModelName = 'oklch'; | |
} else if (convert.rgb.lch) { | |
console.warn("Oklch direct conversion not found for RGB, falling back to LCH for line: " + line); | |
convertedColor = convert.rgb.lch(rgb); | |
colorModelName = 'lch'; | |
} else { | |
throw new Error('Neither Oklch nor LCH conversion available for RGB.'); | |
} | |
const lCss = convertedColor[0].toFixed(2); | |
const cCss = colorModelName === 'oklch' ? (convertedColor[1] / 100).toFixed(4) : convertedColor[1].toFixed(2); | |
const hCss = convertedColor[2].toFixed(2); | |
let colorString = `${colorModelName}(${lCss}% ${cCss} ${hCss}`; | |
if (alphaFromHex !== null && alphaFromHex < 1.0) { | |
colorString += ` / ${(alphaFromHex * 100).toFixed(0)}%`; | |
} | |
colorString += `)`; | |
newLine = `${prefix}${colorString}${suffix}`; | |
} catch (e) { | |
console.warn(`Could not convert HEX value on line: ${line}. Error: ${e.message}`); | |
} | |
} | |
newLines.push(newLine); | |
} | |
fs.writeFileSync(absoluteOutputPath, newLines.join('\n'), 'utf8'); | |
console.log(`Successfully converted colors. Output written to ${absoluteOutputPath}`); | |
} catch (error) { | |
console.error(`Error processing file: ${error.message}`); | |
process.exit(1); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment