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-theme-data.ts

530lines ยท modecode

1#!/usr/bin/env npx tsx
2/**
3 * Build Theme Data
4 *
5 * Generates theme-data.json from CSS source files at BUILD TIME.
6 * This eliminates hardcoded numeric values in the Figma plugin.
7 *
8 * Sources:
9 * - packages/kumo/src/styles/theme-kumo.css (Kumo overrides)
10 * - node_modules/tailwindcss/theme.css (Tailwind v4 defaults)
11 * - packages/kumo/src/components/button/button.tsx (compactSize)
12 *
13 * Output:
14 * packages/kumo-figma/src/generated/theme-data.json
15 *
16 * Usage:
17 * pnpm run build:data (from packages/kumo-figma)
18 */
19
20import { writeFileSync, readFileSync, mkdirSync, readdirSync } from "node:fs";
21import { join, dirname } from "node:path";
22import { fileURLToPath } from "node:url";
23
24const __filename = fileURLToPath(import.meta.url);
25const __dirname = dirname(__filename);
26
27// Paths - reference sibling kumo package
28const KUMO_PKG = join(__dirname, "../../kumo");
29const KUMO_THEME_CSS = join(KUMO_PKG, "src/styles/theme-kumo.css");
30const BUTTON_TSX = join(KUMO_PKG, "src/components/button/button.tsx");
31
32/**
33 * Find Tailwind's theme.css (handles pnpm structure)
34 * Searches both kumo package and monorepo root node_modules
35 */
36function getTailwindThemeCssPath(): string {
37 // Try kumo package's node_modules first
38 const kumoDirectPath = join(KUMO_PKG, "node_modules/tailwindcss/theme.css");
39 try {
40 readFileSync(kumoDirectPath);
41 return kumoDirectPath;
42 } catch {
43 // Ignore and try next location
44 }
45
46 // Try monorepo root node_modules
47 const monorepoRoot = join(__dirname, "../../..");
48 const rootDirectPath = join(
49 monorepoRoot,
50 "node_modules/tailwindcss/theme.css",
51 );
52 try {
53 readFileSync(rootDirectPath);
54 return rootDirectPath;
55 } catch {
56 // Ignore and try pnpm structure
57 }
58
59 // Try pnpm structure in kumo package
60 try {
61 const pnpmBasePath = join(KUMO_PKG, "node_modules/.pnpm");
62 const pnpmDirs = readdirSync(pnpmBasePath);
63 const tailwindDir = pnpmDirs.find((d: string) =>
64 d.startsWith("tailwindcss@"),
65 );
66
67 if (tailwindDir) {
68 return join(
69 pnpmBasePath,
70 tailwindDir,
71 "node_modules/tailwindcss/theme.css",
72 );
73 }
74 } catch {
75 // Ignore and try monorepo pnpm
76 }
77
78 // Try pnpm structure in monorepo root
79 try {
80 const pnpmBasePath = join(monorepoRoot, "node_modules/.pnpm");
81 const pnpmDirs = readdirSync(pnpmBasePath);
82 const tailwindDir = pnpmDirs.find((d: string) =>
83 d.startsWith("tailwindcss@"),
84 );
85
86 if (tailwindDir) {
87 return join(
88 pnpmBasePath,
89 tailwindDir,
90 "node_modules/tailwindcss/theme.css",
91 );
92 }
93 } catch {
94 // Final fallback failed
95 }
96
97 throw new Error(
98 "Could not find tailwindcss/theme.css in kumo package or monorepo root",
99 );
100}
101
102/**
103 * Convert rem to pixels (16px base)
104 */
105function remToPx(remValue: string): number {
106 const num = parseFloat(remValue.replace("rem", ""));
107 return Math.round(num * 16);
108}
109
110/**
111 * Parse Kumo's theme-kumo.css for font sizes
112 * Kumo overrides Tailwind defaults: xs=12, sm=13, base=14, lg=16
113 */
114function parseKumoFontSizes(css: string): Record<string, number> {
115 const sizes: Record<string, number> = {};
116
117 // Match: --text-xs: 12px; or --text-sm: 13px;
118 const pxMatches = css.matchAll(/--text-(\w+):\s*(\d+)px/g);
119 for (const match of pxMatches) {
120 sizes[match[1]] = parseInt(match[2], 10);
121 }
122
123 // Match rem values: --text-xl: 1.25rem;
124 const remMatches = css.matchAll(/--text-(\w+):\s*([\d.]+)rem/g);
125 for (const match of remMatches) {
126 if (!sizes[match[1]]) {
127 sizes[match[1]] = remToPx(match[2] + "rem");
128 }
129 }
130
131 return sizes;
132}
133
134/**
135 * Parse Tailwind v4 theme.css for spacing base unit
136 */
137function parseTailwindSpacing(css: string): { baseUnitPx: number } {
138 // Tailwind v4: --spacing: 0.25rem = 4px
139 const match = css.match(/--spacing:\s*([\d.]+)rem/);
140 if (!match) {
141 throw new Error("Could not find --spacing in tailwind theme.css");
142 }
143 return { baseUnitPx: remToPx(match[1] + "rem") };
144}
145
146/**
147 * Parse Tailwind v4 theme.css for border radius values
148 */
149function parseTailwindBorderRadius(css: string): Record<string, number> {
150 const radii: Record<string, number> = {};
151 const names = ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"];
152
153 for (const name of names) {
154 const pattern = new RegExp(`--radius-${name}:\\s*([\\d.]+)rem`);
155 const match = css.match(pattern);
156 if (match) {
157 radii[name] = remToPx(match[1] + "rem");
158 }
159 }
160
161 return radii;
162}
163
164/**
165 * Parse Tailwind v4 theme.css for font sizes (defaults, may be overridden by Kumo)
166 */
167function parseTailwindFontSizes(css: string): Record<string, number> {
168 const sizes: Record<string, number> = {};
169 const names = [
170 "xs",
171 "sm",
172 "base",
173 "lg",
174 "xl",
175 "2xl",
176 "3xl",
177 "4xl",
178 "5xl",
179 "6xl",
180 "7xl",
181 "8xl",
182 "9xl",
183 ];
184
185 for (const name of names) {
186 const pattern = new RegExp(`--text-${name}:\\s*([\\d.]+)rem`);
187 const match = css.match(pattern);
188 if (match) {
189 sizes[name] = remToPx(match[1] + "rem");
190 }
191 }
192
193 return sizes;
194}
195
196/**
197 * Parse Tailwind v4 theme.css for font weights
198 */
199function parseTailwindFontWeights(css: string): Record<string, number> {
200 const weights: Record<string, number> = {};
201 const names = [
202 "thin",
203 "extralight",
204 "light",
205 "normal",
206 "medium",
207 "semibold",
208 "bold",
209 "extrabold",
210 "black",
211 ];
212
213 for (const name of names) {
214 const pattern = new RegExp(`--font-weight-${name}:\\s*(\\d+)`);
215 const match = css.match(pattern);
216 if (match) {
217 weights[name] = parseInt(match[1], 10);
218 }
219 }
220
221 return weights;
222}
223
224/**
225 * Parse Tailwind v4 theme.css for shadow definitions
226 */
227function parseTailwindShadows(css: string): Record<
228 string,
229 {
230 layers: Array<{
231 offsetX: number;
232 offsetY: number;
233 blur: number;
234 spread: number;
235 opacity: number;
236 }>;
237 }
238> {
239 const shadows: Record<
240 string,
241 {
242 layers: Array<{
243 offsetX: number;
244 offsetY: number;
245 blur: number;
246 spread: number;
247 opacity: number;
248 }>;
249 }
250 > = {};
251 const names = ["2xs", "xs", "sm", "md", "lg", "xl", "2xl"];
252
253 for (const name of names) {
254 const pattern = new RegExp(`--shadow-${name}:\\s*([^;]+);`);
255 const match = css.match(pattern);
256 if (match) {
257 shadows[name] = parseShadowString(match[1].trim());
258 }
259 }
260
261 return shadows;
262}
263
264/**
265 * Parse CSS shadow string into structured layers
266 */
267function parseShadowString(shadowStr: string): {
268 layers: Array<{
269 offsetX: number;
270 offsetY: number;
271 blur: number;
272 spread: number;
273 opacity: number;
274 }>;
275} {
276 const layers: Array<{
277 offsetX: number;
278 offsetY: number;
279 blur: number;
280 spread: number;
281 opacity: number;
282 }> = [];
283
284 // Split by comma for multi-layer shadows
285 const layerStrings = shadowStr.split(/,\s*(?=\d)/);
286
287 for (const layer of layerStrings) {
288 const match = layer.match(
289 /(-?[\d.]+)(?:px)?\s+(-?[\d.]+)(?:px)?\s+(-?[\d.]+)(?:px)?(?:\s+(-?[\d.]+)(?:px)?)?\s+rgb\([^/]+\/\s*([\d.]+)\)/,
290 );
291
292 if (match) {
293 layers.push({
294 offsetX: parseFloat(match[1]),
295 offsetY: parseFloat(match[2]),
296 blur: parseFloat(match[3]),
297 spread: match[4] ? parseFloat(match[4]) : 0,
298 opacity: parseFloat(match[5]),
299 });
300 }
301 }
302
303 return { layers };
304}
305
306/**
307 * Generate full spacing scale from base unit
308 */
309function generateSpacingScale(baseUnitPx: number): Record<string, number> {
310 const keys = [
311 "0",
312 "px",
313 "0.5",
314 "1",
315 "1.5",
316 "2",
317 "2.5",
318 "3",
319 "3.5",
320 "4",
321 "4.5",
322 "5",
323 "6",
324 "6.5", // Kumo custom
325 "7",
326 "8",
327 "9",
328 "10",
329 "11",
330 "12",
331 "14",
332 "16",
333 "20",
334 "24",
335 "28",
336 "32",
337 "36",
338 "40",
339 "44",
340 "48",
341 "52",
342 "56",
343 "60",
344 "64",
345 "72",
346 "80",
347 "96",
348 ];
349
350 const scale: Record<string, number> = {};
351
352 for (const key of keys) {
353 if (key === "0") {
354 scale[key] = 0;
355 } else if (key === "px") {
356 scale[key] = 1;
357 } else {
358 scale[key] = Math.round(parseFloat(key) * baseUnitPx);
359 }
360 }
361
362 return scale;
363}
364
365/**
366 * Parse button.tsx for compactSize values
367 */
368function parseButtonCompactSizes(tsx: string): Record<string, number> {
369 const sizes: Record<string, number> = {};
370 const sizeNames = ["xs", "sm", "base", "lg"];
371
372 for (const name of sizeNames) {
373 // Match: xs: { classes: "size-3.5" }
374 const pattern = new RegExp(
375 `${name}:\\s*\\{\\s*classes:\\s*["']size-([\\d.]+)["']`,
376 );
377 const match = tsx.match(pattern);
378 if (match) {
379 // Convert Tailwind size to pixels: size-X = X * 4
380 sizes[name] = Math.round(parseFloat(match[1]) * 4);
381 }
382 }
383
384 return sizes;
385}
386
387// Main execution
388console.log("๐Ÿ“– Parsing CSS source files for Figma plugin...\n");
389
390// Read source files
391const kumoThemeCss = readFileSync(KUMO_THEME_CSS, "utf-8");
392const tailwindThemeCss = readFileSync(getTailwindThemeCssPath(), "utf-8");
393const buttonTsx = readFileSync(BUTTON_TSX, "utf-8");
394
395// Parse Tailwind v4 defaults
396console.log("๐Ÿ“ฆ Parsing Tailwind v4 theme.css...");
397const tailwindSpacing = parseTailwindSpacing(tailwindThemeCss);
398const tailwindBorderRadius = parseTailwindBorderRadius(tailwindThemeCss);
399const tailwindFontSizes = parseTailwindFontSizes(tailwindThemeCss);
400const tailwindFontWeights = parseTailwindFontWeights(tailwindThemeCss);
401const tailwindShadows = parseTailwindShadows(tailwindThemeCss);
402
403console.log(` - Base spacing unit: ${tailwindSpacing.baseUnitPx}px`);
404console.log(
405 ` - Border radii: ${Object.keys(tailwindBorderRadius).length} values`,
406);
407console.log(` - Font sizes: ${Object.keys(tailwindFontSizes).length} values`);
408console.log(
409 ` - Font weights: ${Object.keys(tailwindFontWeights).length} values`,
410);
411console.log(` - Shadows: ${Object.keys(tailwindShadows).length} values`);
412
413// Parse Kumo overrides (typography is now in theme-kumo.css, generated by codegen:themes)
414console.log("\n๐ŸŽจ Parsing Kumo theme-kumo.css overrides...");
415const kumoFontSizes = parseKumoFontSizes(kumoThemeCss);
416console.log(
417 ` - Font size overrides: xs=${kumoFontSizes.xs}px, sm=${kumoFontSizes.sm}px, base=${kumoFontSizes.base}px, lg=${kumoFontSizes.lg}px`,
418);
419
420// Parse button compact sizes
421console.log("\n๐Ÿ”˜ Parsing button.tsx compact sizes...");
422const buttonCompactSizes = parseButtonCompactSizes(buttonTsx);
423console.log(
424 ` - Compact sizes: xs=${buttonCompactSizes.xs}px, sm=${buttonCompactSizes.sm}px, base=${buttonCompactSizes.base}px, lg=${buttonCompactSizes.lg}px`,
425);
426
427// Generate full spacing scale
428const spacingScale = generateSpacingScale(tailwindSpacing.baseUnitPx);
429
430// Build final theme data
431const themeData = {
432 _generated: new Date().toISOString(),
433 _sources: [
434 "packages/kumo/src/styles/theme-kumo.css",
435 "node_modules/tailwindcss/theme.css",
436 "packages/kumo/src/components/button/button.tsx",
437 ],
438
439 // Tailwind v4 base values
440 tailwind: {
441 spacing: {
442 baseUnitPx: tailwindSpacing.baseUnitPx,
443 scale: spacingScale,
444 },
445 borderRadius: {
446 ...tailwindBorderRadius,
447 full: 9999,
448 none: 0,
449 },
450 fontSize: tailwindFontSizes,
451 fontWeight: tailwindFontWeights,
452 shadows: tailwindShadows,
453 },
454
455 // Kumo-specific overrides
456 kumo: {
457 fontSize: kumoFontSizes,
458 buttonCompactSize: buttonCompactSizes,
459 },
460
461 // Pre-computed values for direct import in generators
462 // These are the "final" values combining Tailwind defaults + Kumo overrides
463 computed: {
464 // For shared.ts SPACING constant
465 spacing: {
466 xs: spacingScale["1"], // gap-1 = 4px
467 sm: spacingScale["1.5"], // gap-1.5 = 6px
468 base: spacingScale["2"], // gap-2 = 8px
469 lg: spacingScale["3"], // gap-3 = 12px
470 },
471
472 // For shared.ts BORDER_RADIUS constant (from Tailwind v4)
473 borderRadius: {
474 xs: tailwindBorderRadius.xs, // 2px
475 sm: tailwindBorderRadius.sm, // 4px
476 md: tailwindBorderRadius.md, // 6px
477 lg: tailwindBorderRadius.lg, // 8px
478 xl: tailwindBorderRadius.xl, // 12px
479 full: 9999,
480 },
481
482 // For shared.ts FONT_SIZE constant (Kumo overrides)
483 fontSize: {
484 xs: kumoFontSizes.xs, // 12px
485 sm: kumoFontSizes.sm, // 13px (Kumo override)
486 base: kumoFontSizes.base, // 14px (Kumo override)
487 lg: kumoFontSizes.lg, // 16px (Kumo override)
488 },
489
490 // For shared.ts FALLBACK_VALUES.fontWeight
491 fontWeight: {
492 normal: tailwindFontWeights.normal, // 400
493 medium: tailwindFontWeights.medium, // 500
494 semiBold: tailwindFontWeights.semibold, // 600
495 },
496
497 // For shared.ts FALLBACK_VALUES.buttonCompactSize
498 buttonCompactSize: buttonCompactSizes,
499
500 // For shared.ts SHADOWS (computed from Tailwind)
501 shadows: {
502 xs: tailwindShadows.xs,
503 lg: tailwindShadows.lg,
504 },
505 },
506};
507
508// Ensure generated directory exists
509const generatedDir = join(__dirname, "generated");
510mkdirSync(generatedDir, { recursive: true });
511
512// Write theme data
513const outputPath = join(generatedDir, "theme-data.json");
514writeFileSync(outputPath, JSON.stringify(themeData, null, 2));
515
516console.log(`\nโœ… Wrote ${outputPath}`);
517console.log("\n๐Ÿ“‹ Summary:");
518console.log(` - Spacing scale: ${Object.keys(spacingScale).length} values`);
519console.log(
520 ` - Border radius: ${Object.keys(themeData.computed.borderRadius).length} values`,
521);
522console.log(
523 ` - Font sizes: ${Object.keys(themeData.computed.fontSize).length} values (Kumo overrides)`,
524);
525console.log(
526 ` - Font weights: ${Object.keys(themeData.computed.fontWeight).length} values`,
527);
528console.log(
529 ` - Button compact sizes: ${Object.keys(buttonCompactSizes).length} values`,
530);
531