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/src/build-figma-variables.ts

264lines ยท modecode

1#!/usr/bin/env npx tsx
2/**
3 * Build Figma Variables Data
4 *
5 * Generates figma-variables.json from the theme generator config.
6 * This data is used by the Figma plugin to create variable collections at runtime.
7 *
8 * Source:
9 * packages/kumo/scripts/theme-generator/config.ts
10 *
11 * Output:
12 * packages/kumo-figma/src/generated/figma-variables.json
13 *
14 * Usage:
15 * pnpm run build:variables (from packages/kumo-figma)
16 */
17
18import { writeFileSync, mkdirSync } from "node:fs";
19import { join, dirname } from "node:path";
20import { fileURLToPath } from "node:url";
21import { THEME_CONFIG } from "@cloudflare/kumo/scripts/theme-generator/config";
22
23const __filename = fileURLToPath(import.meta.url);
24const __dirname = dirname(__filename);
25
26// =============================================================================
27// Color Conversion Utilities
28// =============================================================================
29
30/**
31 * Convert OKLab to linear sRGB
32 */
33function oklabToLinearSrgb(
34 L: number,
35 a: number,
36 b: number,
37): [number, number, number] {
38 const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
39 const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
40 const s_ = L - 0.0894841775 * a - 1.291485548 * b;
41
42 const l = l_ * l_ * l_;
43 const m = m_ * m_ * m_;
44 const s = s_ * s_ * s_;
45
46 return [
47 +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
48 -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
49 -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
50 ];
51}
52
53/**
54 * Convert linear sRGB to sRGB (apply gamma)
55 */
56function linearToSrgb(x: number): number {
57 if (x <= 0.0031308) {
58 return 12.92 * x;
59 }
60 return 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
61}
62
63/**
64 * Convert oklch to RGB (proper conversion for Figma)
65 */
66function oklchToRgb(
67 oklch: string,
68): { r: number; g: number; b: number; a?: number } | null {
69 const match = oklch.match(
70 /oklch\(\s*([\d.]+)%?\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)/,
71 );
72
73 if (!match) return null;
74
75 let L = parseFloat(match[1]);
76 const C = parseFloat(match[2]);
77 const H = parseFloat(match[3]);
78 const alpha = match[4] ? parseFloat(match[4]) : undefined;
79
80 // Normalize L if it's a percentage
81 if (L > 1) L = L / 100;
82
83 // Convert oklch to oklab
84 const hRad = (H * Math.PI) / 180;
85 const a = C * Math.cos(hRad);
86 const b = C * Math.sin(hRad);
87
88 // Convert oklab to linear sRGB
89 const [linearR, linearG, linearB] = oklabToLinearSrgb(L, a, b);
90
91 // Convert linear sRGB to sRGB and clamp
92 const r = Math.max(0, Math.min(1, linearToSrgb(linearR)));
93 const g = Math.max(0, Math.min(1, linearToSrgb(linearG)));
94 const bl = Math.max(0, Math.min(1, linearToSrgb(linearB)));
95
96 const result: { r: number; g: number; b: number; a?: number } = {
97 r,
98 g,
99 b: bl,
100 };
101 if (alpha !== undefined) result.a = alpha;
102 return result;
103}
104
105/**
106 * Convert hex to RGB
107 */
108function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
109 const shortMatch = hex.match(/^#?([a-f\d])([a-f\d])([a-f\d])$/i);
110 if (shortMatch) {
111 return {
112 r: parseInt(shortMatch[1] + shortMatch[1], 16) / 255,
113 g: parseInt(shortMatch[2] + shortMatch[2], 16) / 255,
114 b: parseInt(shortMatch[3] + shortMatch[3], 16) / 255,
115 };
116 }
117
118 const match = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
119 if (!match) return null;
120
121 return {
122 r: parseInt(match[1], 16) / 255,
123 g: parseInt(match[2], 16) / 255,
124 b: parseInt(match[3], 16) / 255,
125 };
126}
127
128/**
129 * Parse a CSS color value to Figma RGB
130 */
131function parseColorToRgb(value: string): {
132 r: number;
133 g: number;
134 b: number;
135 a?: number;
136} {
137 const trimmed = value.trim();
138
139 // Handle hex colors
140 if (trimmed.startsWith("#")) {
141 return hexToRgb(trimmed) || { r: 0.5, g: 0.5, b: 0.5 };
142 }
143
144 // Handle oklch
145 if (trimmed.startsWith("oklch")) {
146 return oklchToRgb(trimmed) || { r: 0.5, g: 0.5, b: 0.5 };
147 }
148
149 // Handle named colors
150 if (trimmed === "transparent") {
151 return { r: 0, g: 0, b: 0, a: 0 };
152 }
153 if (trimmed === "white" || trimmed === "#fff") {
154 return { r: 1, g: 1, b: 1 };
155 }
156 if (trimmed === "black" || trimmed === "#000") {
157 return { r: 0, g: 0, b: 0 };
158 }
159
160 // Handle var() with fallback - extract the fallback value
161 if (trimmed.startsWith("var(")) {
162 const fallbackMatch = trimmed.match(/,\s*(.+)\)$/);
163 if (fallbackMatch) {
164 return parseColorToRgb(fallbackMatch[1]);
165 }
166 }
167
168 // Default fallback
169 return { r: 0.5, g: 0.5, b: 0.5 };
170}
171
172// =============================================================================
173// Types
174// =============================================================================
175
176type FigmaColor = { r: number; g: number; b: number; a?: number };
177
178type ColorVariable = {
179 name: string;
180 light: FigmaColor;
181 dark: FigmaColor;
182};
183
184// =============================================================================
185// Main
186// =============================================================================
187
188function main() {
189 console.log("๐Ÿ“– Building Figma variables from theme config...\n");
190
191 const colorVariables: ColorVariable[] = [];
192
193 // Process text color tokens
194 console.log("๐Ÿ“ Processing text color tokens...");
195 for (const [tokenName, def] of Object.entries(THEME_CONFIG.text)) {
196 const kumoTheme = def.theme.kumo;
197 if (!kumoTheme) continue;
198
199 colorVariables.push({
200 name: `text-color-${tokenName}`,
201 light: parseColorToRgb(kumoTheme.light),
202 dark: parseColorToRgb(kumoTheme.dark),
203 });
204 }
205 console.log(` Found ${Object.keys(THEME_CONFIG.text).length} text tokens`);
206
207 // Process color tokens
208 console.log("๐ŸŽจ Processing color tokens...");
209 for (const [tokenName, def] of Object.entries(THEME_CONFIG.color)) {
210 const kumoTheme = def.theme.kumo;
211 if (!kumoTheme) continue;
212
213 colorVariables.push({
214 name: `color-${tokenName}`,
215 light: parseColorToRgb(kumoTheme.light),
216 dark: parseColorToRgb(kumoTheme.dark),
217 });
218 }
219 console.log(
220 ` Found ${Object.keys(THEME_CONFIG.color).length} color tokens`,
221 );
222
223 console.log(`\nโœ… Total: ${colorVariables.length} color variables`);
224
225 // Create a lookup map for quick access by name
226 const variablesByName: Record<string, ColorVariable> = {};
227 for (const v of colorVariables) {
228 variablesByName[v.name] = v;
229 }
230
231 // Preview
232 console.log("\n๐Ÿ“‹ Sample variables:");
233 for (const v of colorVariables.slice(0, 5)) {
234 console.log(
235 ` - ${v.name}: light(${v.light.r.toFixed(2)}, ${v.light.g.toFixed(2)}, ${v.light.b.toFixed(2)}) dark(${v.dark.r.toFixed(2)}, ${v.dark.g.toFixed(2)}, ${v.dark.b.toFixed(2)})`,
236 );
237 }
238
239 // List all variable names for reference
240 console.log("\n๐Ÿ“‹ All variable names:");
241 for (const v of colorVariables) {
242 console.log(` - ${v.name}`);
243 }
244
245 // Ensure generated directory exists
246 const generatedDir = join(__dirname, "generated");
247 mkdirSync(generatedDir, { recursive: true });
248
249 // Build output
250 const output = {
251 _generated: new Date().toISOString(),
252 _source: "packages/kumo/scripts/theme-generator/config.ts",
253 collectionName: "kumo-colors",
254 variables: colorVariables,
255 byName: variablesByName,
256 };
257
258 const outputPath = join(generatedDir, "figma-variables.json");
259 writeFileSync(outputPath, JSON.stringify(output, null, 2));
260
261 console.log(`\nโœ… Wrote ${outputPath}`);
262}
263
264main();
265