cloudflare/kumo

Public

mirrored fromhttps://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5260f1a5703bb69e6c7f7cf0ce8033a561cac8b5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/kumo-figma/scripts/color-utils.ts

153lines · modecode

1import { rgb, type Oklch } from "culori";
2
3/**
4 * Figma color format with RGB values in 0-1 range
5 */
6export type FigmaColor = { r: number; g: number; b: number; a?: number };
7
8/**
9 * Converts various color formats to Figma RGB format (0-1 range)
10 *
11 * @param value - Color value in oklch, hex, or CSS color format
12 * @returns Figma RGB color object with values in 0-1 range
13 *
14 * @example
15 * resolveColor("oklch(21% 0.006 285.885)") // OKLCH format
16 * resolveColor("oklch(87% 0 0 / 0.8)") // OKLCH with alpha
17 * resolveColor("#f6821f") // Hex color
18 * resolveColor("transparent") // Special case
19 * resolveColor("var(--color-x, #fff)") // CSS variable with fallback
20 */
21export function resolveColor(value: string): FigmaColor {
22 const trimmed = value.trim();
23
24 // Handle transparent
25 if (trimmed === "transparent") {
26 return { r: 0, g: 0, b: 0, a: 0 };
27 }
28
29 // Handle CSS var() with fallback - extract the fallback value
30 if (trimmed.startsWith("var(")) {
31 const fallback = extractCssVarFallback(trimmed);
32 if (!fallback) {
33 throw new Error(`Cannot resolve CSS variable without fallback: ${value}`);
34 }
35 return resolveColor(fallback);
36 }
37
38 // Handle OKLCH format
39 if (trimmed.startsWith("oklch(")) {
40 return convertOklchToFigma(trimmed);
41 }
42
43 // Handle hex colors
44 if (trimmed.startsWith("#")) {
45 return convertHexToFigma(trimmed);
46 }
47
48 throw new Error(`Unsupported color format: ${value}`);
49}
50
51function extractCssVarFallback(value: string): string | null {
52 // CSS syntax: var(--name[, fallback])
53 // The fallback can contain nested functions like oklch(...), so regexes that stop
54 // at the first ')' will truncate. Parse by tracking parentheses depth.
55 const s = value.trim();
56 if (!s.startsWith("var(")) return null;
57
58 const end = s.trimEnd();
59 if (!end.endsWith(")")) return null;
60
61 let depth = 0;
62 let commaIndex = -1;
63 for (let i = 0; i < end.length; i++) {
64 const ch = end[i];
65 if (ch === "(") depth++;
66 else if (ch === ")") depth--;
67 else if (ch === "," && depth === 1) {
68 commaIndex = i;
69 break;
70 }
71 }
72
73 if (commaIndex === -1) return null;
74
75 const closeIndex = end.length - 1; // last ')'
76 return end.slice(commaIndex + 1, closeIndex).trim();
77}
78
79/**
80 * Converts OKLCH color string to Figma RGB format
81 *
82 * @param oklchString - OKLCH color string (e.g., "oklch(21% 0.006 285.885)")
83 * @returns Figma RGB color object
84 */
85function convertOklchToFigma(oklchString: string): FigmaColor {
86 // Extract values from oklch(L C H / alpha) or oklch(L C H)
87 const match = oklchString.match(
88 /oklch\(\s*([0-9.]+%?)\s+([0-9.]+)\s+([0-9.]+)\s*(?:\/\s*([0-9.]+))?\s*\)/,
89 );
90
91 if (!match) {
92 throw new Error(`Invalid OKLCH format: ${oklchString}`);
93 }
94
95 let [, lightnessStr, chromaStr, hueStr, alphaStr] = match;
96
97 // Parse lightness (handle percentage)
98 let lightness = parseFloat(lightnessStr);
99 if (lightnessStr.includes("%")) {
100 lightness = lightness / 100;
101 }
102
103 const chroma = parseFloat(chromaStr);
104 const hue = parseFloat(hueStr);
105 const alpha = alphaStr ? parseFloat(alphaStr) : undefined;
106
107 // Convert OKLCH to RGB using culori
108 const oklchColor: Oklch = { mode: "oklch", l: lightness, c: chroma, h: hue };
109 const rgbColor = rgb(oklchColor);
110
111 if (!rgbColor) {
112 throw new Error(`Failed to convert OKLCH to RGB: ${oklchString}`);
113 }
114
115 // Clamp RGB values to 0-1 range (OKLCH can produce out-of-gamut colors)
116 const result: FigmaColor = {
117 r: Math.max(0, Math.min(1, rgbColor.r)),
118 g: Math.max(0, Math.min(1, rgbColor.g)),
119 b: Math.max(0, Math.min(1, rgbColor.b)),
120 };
121
122 if (alpha !== undefined) {
123 result.a = alpha;
124 }
125
126 return result;
127}
128
129/**
130 * Converts hex color to Figma RGB format
131 *
132 * @param hex - Hex color string (e.g., "#fff", "#f6821f")
133 * @returns Figma RGB color object
134 */
135function convertHexToFigma(hex: string): FigmaColor {
136 // Expand shorthand hex (#fff -> #ffffff)
137 let normalized = hex.replace(
138 /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i,
139 "#$1$1$2$2$3$3",
140 );
141
142 const match = normalized.match(/^#([0-9a-f]{6})$/i);
143 if (!match) {
144 throw new Error(`Invalid hex color: ${hex}`);
145 }
146
147 const hexValue = match[1];
148 const r = parseInt(hexValue.substring(0, 2), 16) / 255;
149 const g = parseInt(hexValue.substring(2, 4), 16) / 255;
150 const b = parseInt(hexValue.substring(4, 6), 16) / 255;
151
152 return { r, g, b };
153}
154