[READ-ONLY] a fast, modern browser for the npm registry
at main 197 lines 6.4 kB view raw
1// Vue Data UI does not support CSS vars nor OKLCH for now 2 3/** 4 * Default neutral OKLCH color used as fallback when CSS variables are unavailable (e.g., during SSR). 5 * This matches the dark mode value of --fg-subtle defined in main.css. 6 */ 7export const OKLCH_NEUTRAL_FALLBACK = 'oklch(0.633 0 0)' 8 9/** Converts a 0-255 RGB component to a 2-digit hex string */ 10const componentToHex = (value: number): string => 11 Math.round(Math.min(Math.max(0, value), 255)) 12 .toString(16) 13 .padStart(2, '0') 14 15/** Converts a 0-1 linear value to a 2-digit hex string */ 16const linearToHex = (value: number): string => 17 Math.round(Math.min(Math.max(0, value), 1) * 255) 18 .toString(16) 19 .padStart(2, '0') 20 21/** Converts linear RGB to sRGB gamma-corrected value */ 22const linearToSrgb = (value: number): number => 23 value <= 0.0031308 ? 12.92 * value : 1.055 * Math.pow(value, 1 / 2.4) - 0.055 24 25/** 26 * Converts a hex color to RGB components 27 */ 28function hexToRgb(hex: string): [number, number, number] | null { 29 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) 30 return result 31 ? [parseInt(result[1]!, 16), parseInt(result[2]!, 16), parseInt(result[3]!, 16)] 32 : null 33} 34 35/** 36 * Converts RGB components to hex color 37 */ 38function rgbToHex(r: number, g: number, b: number): string { 39 return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}` 40} 41 42/** 43 * Lightens a hex color by mixing it with white. 44 * Used to create light tints of accent colors for better visibility in light mode. 45 * @param hex - The hex color to lighten (e.g., "#ff0000") 46 * @param factor - Lighten factor from 0 to 1 (0.5 = 50% lighter, mixed with white) 47 */ 48export function lightenHex(hex: string, factor: number = 0.5): string { 49 const rgb = hexToRgb(hex) 50 if (!rgb) return hex 51 52 // Lighten by mixing with white (255, 255, 255) 53 const lightened = rgb.map(c => Math.round(c + (255 - c) * factor)) as [number, number, number] 54 return rgbToHex(...lightened) 55} 56 57export function oklchToHex(color: string | undefined | null): string | undefined | null { 58 if (color == null) return color 59 60 const match = color.trim().match(/^oklch\(\s*([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\s*\)$/i) 61 62 if (!match) { 63 throw new Error('Invalid OKLCH color format') 64 } 65 66 const lightness = Number(match[1]) 67 const chroma = Number(match[2]) 68 const hue = Number(match[3]) 69 70 const hRad = (hue * Math.PI) / 180 71 72 const a = chroma * Math.cos(hRad) 73 const b = chroma * Math.sin(hRad) 74 75 let l_ = lightness + 0.3963377774 * a + 0.2158037573 * b 76 let m_ = lightness - 0.1055613458 * a - 0.0638541728 * b 77 let s_ = lightness - 0.0894841775 * a - 1.291485548 * b 78 79 l_ = l_ ** 3 80 m_ = m_ ** 3 81 s_ = s_ ** 3 82 83 const r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_ 84 const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_ 85 const bRgb = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_ 86 87 return `#${linearToHex(linearToSrgb(r))}${linearToHex(linearToSrgb(g))}${linearToHex(linearToSrgb(bRgb))}` 88} 89 90/** 91 * Lighten an OKLCH color by a given factor. 92 * 93 * Works with strict TypeScript settings including `noUncheckedIndexedAccess`, 94 * where `match[n]` is typed as `string | undefined`. 95 * 96 * @param oklch - Color in the form "oklch(L C H)" or "oklch(L C H / A)" 97 * @param factor - Lightening force in range [0, 1] 98 * @returns Lightened OKLCH color string (0.5 = 50% lighter) 99 */ 100export function lightenOklch( 101 oklch: string | null | undefined, 102 factor: number, 103): string | null | undefined { 104 if (oklch == null) { 105 return oklch 106 } 107 108 const input = oklch.trim() 109 110 const match = input.match( 111 /^oklch\(\s*([+-]?[\d.]+%?)\s+([+-]?[\d.]+)\s+([+-]?[\d.]+)(?:\s*\/\s*([+-]?[\d.]+%?))?\s*\)$/i, 112 ) 113 114 if (!match) { 115 throw new Error('Invalid OKLCH color format') 116 } 117 118 const [, lightnessText, chromaText, hueText, alphaText] = match 119 120 if (lightnessText === undefined || chromaText === undefined || hueText === undefined) { 121 throw new Error('Invalid OKLCH color format') 122 } 123 124 let lightness = lightnessText.endsWith('%') 125 ? Number.parseFloat(lightnessText) / 100 126 : Number.parseFloat(lightnessText) 127 let chroma = Number.parseFloat(chromaText) 128 const hue = Number.parseFloat(hueText) 129 const alpha = 130 alphaText === undefined 131 ? null 132 : alphaText.endsWith('%') 133 ? Number.parseFloat(alphaText) / 100 134 : Number.parseFloat(alphaText) 135 136 const clampedFactor = Math.min(Math.max(factor, 0), 1) 137 lightness = lightness + (1 - lightness) * clampedFactor 138 139 // Reduce chroma slightly as lightness increases 140 chroma = chroma * (1 - clampedFactor * 0.3) 141 142 lightness = Math.min(Math.max(lightness, 0), 1) 143 chroma = Math.max(chroma, 0) 144 145 return alpha === null 146 ? `oklch(${lightness} ${chroma} ${hue})` 147 : `oklch(${lightness} ${chroma} ${hue} / ${alpha})` 148} 149 150/** 151 * Make an OKLCH color transparent by a given factor. 152 * 153 * @param oklch - Color in the form "oklch(L C H)" or "oklch(L C H / A)" 154 * @param factor - Transparency factor in range [0, 1] 155 * @returns OKLCH color string with adjusted alpha (0.5 = 50% transparency, 1 = fully transparent) 156 */ 157export function transparentizeOklch( 158 oklch: string | null | undefined, 159 factor: number, 160 fallback = 'oklch(0 0 0 / 0)', 161): string { 162 if (oklch == null) return fallback 163 164 const input = oklch.trim() 165 if (!input) return fallback 166 167 const match = input.match( 168 /^oklch\(\s*([+-]?[\d.]+%?)\s+([+-]?[\d.]+)\s+([+-]?[\d.]+)(?:\s*\/\s*([+-]?[\d.]+%?))?\s*\)$/i, 169 ) 170 171 if (!match) return fallback 172 173 const [, lightnessText, chromaText, hueText, alphaText] = match 174 175 if (lightnessText === undefined || chromaText === undefined || hueText === undefined) { 176 return fallback 177 } 178 179 const lightness = lightnessText.endsWith('%') 180 ? Math.min(Math.max(Number.parseFloat(lightnessText) / 100, 0), 1) 181 : Math.min(Math.max(Number.parseFloat(lightnessText), 0), 1) 182 183 const chroma = Math.max(0, Number.parseFloat(chromaText)) 184 const hue = Number.parseFloat(hueText) 185 186 const originalAlpha = 187 alphaText === undefined 188 ? 1 189 : alphaText.endsWith('%') 190 ? Math.min(Math.max(Number.parseFloat(alphaText) / 100, 0), 1) 191 : Math.min(Math.max(Number.parseFloat(alphaText), 0), 1) 192 193 const clampedFactor = Math.min(Math.max(factor, 0), 1) 194 const alpha = Math.max(0, originalAlpha * (1 - clampedFactor)) 195 196 return `oklch(${lightness} ${chroma} ${hue} / ${alpha})` 197}