@nebutra/design-tokens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,697 @@
1
+ /**
2
+ * Style Dictionary v4 build configuration
3
+ *
4
+ * Three platforms (CSS / TS / Tailwind preset) consume the same DTCG tokens.
5
+ * Output goes to ./build/* and to packages/design/tokens/styles.css and packages/design/theme/themes.css
6
+ * via concat-and-replace once parity is at 99%+.
7
+ *
8
+ * Build modes:
9
+ * light → :root (core + semantic + light theme)
10
+ * dark → .dark (core + semantic + dark theme)
11
+ * <multi> → [data-theme="<name>"] (one of: neon, gradient, dark-dense, minimal, vibrant, ocean)
12
+ *
13
+ * Modeling extensions handled by post-processing:
14
+ * - $extensions["com.nebutra.display-p3"] → emit @supports (color: color(display-p3 ...)) override
15
+ * - $extensions["com.nebutra.oklch"] → emit @supports (color: oklch(0 0 0)) override
16
+ * - composite transition / focusRing → emit shorthand --transition / --focus-ring
17
+ * - tailwind @theme inline → emit `@theme inline { ... }` block referencing tokens
18
+ *
19
+ * Reference: https://styledictionary.com/reference/config/
20
+ */
21
+
22
+ import { readFile, writeFile } from "node:fs/promises";
23
+ import StyleDictionary from "style-dictionary";
24
+
25
+ const MULTI_THEMES = ["neon", "gradient", "dark-dense", "minimal", "vibrant", "ocean"];
26
+
27
+ /**
28
+ * Custom CSS variable namer.
29
+ *
30
+ * Maps DTCG paths to the same names the legacy SSOT uses.
31
+ */
32
+ function pathToCssVarName(token) {
33
+ const path = token.path;
34
+ const [head, ...rest] = path;
35
+
36
+ // Drop primitive `color.` prefix → keep child name.
37
+ if (head === "color") {
38
+ if (rest[0] === "white" || rest[0] === "black") return null;
39
+ if (rest[0] === "tertiary-purple") return "brand-tertiary";
40
+ return rest.join("-");
41
+ }
42
+
43
+ if (head === "size") return rest.join("-");
44
+ if (head === "duration") return `duration-${rest.join("-")}`;
45
+ if (head === "easing") return `ease-${rest.join("-")}`;
46
+ if (head === "fontFamily") return `font-${rest.join("-")}`;
47
+
48
+ if (head === "brand") {
49
+ if (rest[0] === "gradient") {
50
+ const sub = rest.slice(1).join("-");
51
+ return sub === "primary" ? "brand-gradient" : `brand-gradient-${sub}`;
52
+ }
53
+ return `brand-${rest.join("-")}`;
54
+ }
55
+
56
+ if (head === "status") return `status-${rest.join("-")}`;
57
+
58
+ if (head === "container") return `container-${rest.join("-")}`;
59
+ if (head === "radius") {
60
+ return rest[0] === "default" ? "radius" : `radius-${rest.join("-")}`;
61
+ }
62
+ if (head === "transition") {
63
+ if (rest[0] === "shorthand") return "transition";
64
+ if (rest[0] === "default") return null; // composite — emitted via post-process
65
+ return `transition-${rest.join("-")}`;
66
+ }
67
+ if (head === "focusRing") {
68
+ if (rest[0] === "default") return "focus-ring";
69
+ return `focus-ring-${rest.join("-")}`;
70
+ }
71
+
72
+ if (head === "scale") return rest.join("-");
73
+ if (head === "shadcn") return rest.join("-");
74
+ if (head === "ds") return `ds-${rest.join("-")}`;
75
+ if (head === "elevation") return `elevation-${rest.join("-")}`;
76
+
77
+ if (head === "theme") return null;
78
+
79
+ if (head === "shadow") return `shadow-${rest.join("-")}`;
80
+
81
+ return path.join("-");
82
+ }
83
+
84
+ StyleDictionary.registerTransform({
85
+ name: "name/nebutra/css",
86
+ type: "name",
87
+ transform: (token) => pathToCssVarName(token) ?? `__skip__${token.path.join("-")}`,
88
+ });
89
+
90
+ /**
91
+ * Multi-theme CSS variable namer.
92
+ *
93
+ * Multi-theme files (neon, gradient, dark-dense, minimal, vibrant, ocean) emit
94
+ * Tailwind v4 \`@theme\`-compatible names: color.primary → --color-primary,
95
+ * radius.md → --radius-md, fontFamily.sans → --font-sans, shadow.sm → --shadow-sm,
96
+ * transition.fast → --transition-fast.
97
+ */
98
+ function multiThemeName(token) {
99
+ const path = token.path;
100
+ const [head, ...rest] = path;
101
+ if (head === "theme") return null;
102
+ if (head === "color") return `color-${rest.join("-")}`;
103
+ if (head === "radius") return `radius-${rest.join("-")}`;
104
+ if (head === "fontFamily") return `font-${rest.join("-")}`;
105
+ if (head === "shadow") return `shadow-${rest.join("-")}`;
106
+ if (head === "transition") return `transition-${rest.join("-")}`;
107
+ return path.join("-");
108
+ }
109
+
110
+ StyleDictionary.registerTransform({
111
+ name: "name/nebutra/css/multi-theme",
112
+ type: "name",
113
+ transform: (token) => multiThemeName(token) ?? `__skip__${token.path.join("-")}`,
114
+ });
115
+
116
+ const filterSkipped = (token) => !token.name?.startsWith("__skip__");
117
+
118
+ StyleDictionary.registerTransform({
119
+ name: "color/nebutra/passthrough",
120
+ type: "value",
121
+ filter: (token) => token.$type === "color" || token.type === "color",
122
+ transform: (token) => {
123
+ const value = token.$value ?? token.value;
124
+ return typeof value === "string" ? value : value;
125
+ },
126
+ });
127
+
128
+ StyleDictionary.registerTransform({
129
+ name: "string/nebutra/passthrough",
130
+ type: "value",
131
+ filter: (token) => token.$type === "string" || token.type === "string",
132
+ transform: (token) => token.$value ?? token.value,
133
+ });
134
+
135
+ const buildMode = ({
136
+ mode,
137
+ selector,
138
+ sources,
139
+ outputFile,
140
+ nameTransform = "name/nebutra/css",
141
+ }) => ({
142
+ log: { verbosity: "default", warnings: "warn" },
143
+ preprocessors: ["tokens-studio"],
144
+ source: sources,
145
+ platforms: {
146
+ css: {
147
+ transforms: [
148
+ "attribute/cti",
149
+ "color/nebutra/passthrough",
150
+ "string/nebutra/passthrough",
151
+ nameTransform,
152
+ ],
153
+ buildPath: "build/css/",
154
+ options: { usesDtcg: true },
155
+ files: [
156
+ {
157
+ destination: outputFile,
158
+ format: "css/variables",
159
+ filter: filterSkipped,
160
+ options: {
161
+ selector,
162
+ outputReferences: false,
163
+ fileHeader: () => [
164
+ "@nebutra/design-tokens — generated from W3C DTCG tokens.",
165
+ "DO NOT EDIT — edit tokens/*.json and re-run `pnpm build`.",
166
+ ],
167
+ },
168
+ },
169
+ ],
170
+ },
171
+ ts: {
172
+ transforms: [
173
+ "attribute/cti",
174
+ "color/nebutra/passthrough",
175
+ "string/nebutra/passthrough",
176
+ "name/pascal",
177
+ ],
178
+ buildPath: "build/ts/",
179
+ options: { usesDtcg: true },
180
+ files: [
181
+ { destination: `${mode}.ts`, format: "javascript/es6" },
182
+ { destination: `${mode}.d.ts`, format: "typescript/es6-declarations" },
183
+ ],
184
+ },
185
+ tailwind: {
186
+ transforms: [
187
+ "attribute/cti",
188
+ "color/nebutra/passthrough",
189
+ "string/nebutra/passthrough",
190
+ nameTransform,
191
+ ],
192
+ buildPath: "build/tailwind/",
193
+ options: { usesDtcg: true },
194
+ files: [
195
+ {
196
+ destination: `${mode}.preset.cjs`,
197
+ format: "javascript/module-flat",
198
+ filter: filterSkipped,
199
+ },
200
+ ],
201
+ },
202
+ },
203
+ });
204
+
205
+ const configs = [
206
+ buildMode({
207
+ mode: "light",
208
+ selector: ":root",
209
+ sources: ["tokens/core.json", "tokens/semantic.json", "tokens/themes/light.json"],
210
+ outputFile: "light.css",
211
+ }),
212
+ buildMode({
213
+ mode: "dark",
214
+ selector: ".dark",
215
+ sources: ["tokens/core.json", "tokens/semantic.json", "tokens/themes/dark.json"],
216
+ outputFile: "dark.css",
217
+ }),
218
+ // Multi-theme builds: include ONLY the theme file (not core/semantic primitives).
219
+ // The brand primitives are emitted once via the light/dark builds and shared
220
+ // across all themes via cascade. Multi-theme files only override theme-scoped
221
+ // tokens (--color-*, --radius-*, --font-*, etc.).
222
+ // Multi-theme builds: include ONLY the theme file (not core/semantic primitives).
223
+ // Use the multi-theme namer so color.primary → --color-primary (Tailwind v4 @theme).
224
+ ...MULTI_THEMES.map((name) =>
225
+ buildMode({
226
+ mode: name,
227
+ selector: name === "neon" ? "@theme" : `[data-theme="${name}"]`,
228
+ sources: [`tokens/themes/${name}.json`],
229
+ outputFile: `${name}.css`,
230
+ nameTransform: "name/nebutra/css/multi-theme",
231
+ }),
232
+ ),
233
+ ];
234
+
235
+ for (const cfg of configs) {
236
+ const sd = new StyleDictionary(cfg);
237
+ await sd.hasInitialized;
238
+ await sd.cleanAllPlatforms();
239
+ await sd.buildAllPlatforms();
240
+ }
241
+
242
+ // ─── Post-processing ─────────────────────────────────────────────────────────
243
+ //
244
+ // SD's css/variables format does not natively model:
245
+ // - @supports (color: color(display-p3 ...)) wrappers
246
+ // - @supports (color: oklch(0 0 0)) wrappers
247
+ // - Token aliases that compute from primitive values (--nebutra-brand-blue)
248
+ // - Composite shorthand tokens (--transition shorthand)
249
+ // - Tailwind v4 `@theme inline { ... }` blocks
250
+ //
251
+ // We post-process the generated CSS to inject these constructs.
252
+
253
+ /** Parse a JSON file and return its content. */
254
+ async function readJson(filePath) {
255
+ const txt = await readFile(filePath, "utf8");
256
+ return JSON.parse(txt);
257
+ }
258
+
259
+ /**
260
+ * Walk a DTCG token tree; for each leaf with $value, emit
261
+ * { path: ["color","nebutra-blue","500"], leaf: { $value: ..., $extensions: ... } }
262
+ */
263
+ function* walkTokens(node, path = []) {
264
+ if (node === null || typeof node !== "object") return;
265
+ if ("$value" in node) {
266
+ yield { path, leaf: node };
267
+ return;
268
+ }
269
+ for (const [key, child] of Object.entries(node)) {
270
+ if (key.startsWith("$")) continue;
271
+ yield* walkTokens(child, [...path, key]);
272
+ }
273
+ }
274
+
275
+ /** Build `varName → P3 value` map from core.json. */
276
+ async function buildP3Map() {
277
+ const core = await readJson("tokens/core.json");
278
+ const map = new Map();
279
+ for (const { path, leaf } of walkTokens(core)) {
280
+ const p3 = leaf?.$extensions?.["com.nebutra.display-p3"];
281
+ if (!p3) continue;
282
+ const name = pathToCssVarName({ path });
283
+ if (name && !name.startsWith("__skip__")) map.set(name, p3);
284
+ }
285
+ return map;
286
+ }
287
+
288
+ /** Build `varName → oklch value` map for a given theme file. */
289
+ async function buildOklchMap(themePath) {
290
+ const theme = await readJson(themePath);
291
+ const map = new Map();
292
+ for (const { path, leaf } of walkTokens(theme)) {
293
+ const ok = leaf?.$extensions?.["com.nebutra.oklch"];
294
+ if (!ok) continue;
295
+ const name = pathToCssVarName({ path });
296
+ if (name && !name.startsWith("__skip__")) map.set(name, ok);
297
+ }
298
+ return map;
299
+ }
300
+
301
+ /**
302
+ * Inject the post-processing extras (P3 brand bridge, oklch ds-gray, gradient aliases,
303
+ * --transition / --focus-ring, --nebutra-brand-blue / --nebutra-brand-cyan) into the
304
+ * generated mode CSS for the given selector.
305
+ */
306
+ function buildExtras({ selector, p3Map, oklchMap, includeBrandBridge, isDark }) {
307
+ const lines = [];
308
+
309
+ // 1. --nebutra-brand-blue / --nebutra-brand-cyan aliases (only in :root for parity).
310
+ if (includeBrandBridge) {
311
+ lines.push(
312
+ `${selector} {`,
313
+ ` /* Wide-gamut bridge: default to sRGB, upgrade to Display-P3 when available. */`,
314
+ ` --nebutra-brand-blue: var(--nebutra-blue-500);`,
315
+ ` --nebutra-brand-cyan: var(--nebutra-cyan-500);`,
316
+ `}`,
317
+ ``,
318
+ );
319
+ }
320
+
321
+ // 2. --gradient-brand-* aliases (Tailwind @theme convention used by docs apps).
322
+ // These mirror --brand-gradient-* so existing consumers keep working.
323
+ lines.push(
324
+ `${selector} {`,
325
+ ` /* Gradient aliases — both --brand-gradient-* (canonical, CLAUDE.md) and --gradient-brand-* (legacy) are emitted. */`,
326
+ ` --gradient-brand: var(--brand-gradient);`,
327
+ ` --gradient-brand-hover: var(--brand-gradient-reverse);`,
328
+ ` --gradient-brand-vertical: var(--brand-gradient-vertical);`,
329
+ ` --gradient-brand-reverse: var(--brand-gradient-reverse);`,
330
+ ` --gradient-brand-radial: var(--brand-gradient-radial);`,
331
+ ` --gradient-brand-glow: var(--brand-gradient-radial);`,
332
+ ` --gradient-section: var(--brand-gradient-vertical);`,
333
+ ` --gradient-glow: var(--brand-gradient-radial);`,
334
+ `}`,
335
+ ``,
336
+ );
337
+
338
+ // 3. --transition shorthand + --focus-ring composite.
339
+ if (selector === ":root") {
340
+ lines.push(
341
+ `${selector} {`,
342
+ ` /* Composite tokens — combine duration + easing for shorthand use. */`,
343
+ ` --transition: 150ms ease-out;`,
344
+ ` --focus-ring: 0 0 0 2px hsl(var(--ring) / 0.4);`,
345
+ `}`,
346
+ ``,
347
+ );
348
+ }
349
+
350
+ // 4. P3 wide-gamut overrides (only emit once, in the lightest selector).
351
+ if (includeBrandBridge && p3Map && p3Map.size > 0) {
352
+ lines.push(
353
+ `@supports (color: color(display-p3 1 1 1)) {`,
354
+ ` ${selector} {`,
355
+ ` /* Richer brand values on Display-P3 hardware; sRGB fallback above. */`,
356
+ );
357
+ for (const [name, value] of p3Map) {
358
+ lines.push(` --${name}: ${value};`);
359
+ }
360
+ // Bridge tokens get P3 values too.
361
+ if (p3Map.has("nebutra-blue-500")) {
362
+ lines.push(` --nebutra-brand-blue: ${p3Map.get("nebutra-blue-500")};`);
363
+ }
364
+ if (p3Map.has("nebutra-cyan-500")) {
365
+ lines.push(` --nebutra-brand-cyan: ${p3Map.get("nebutra-cyan-500")};`);
366
+ }
367
+ lines.push(` }`, `}`, ``);
368
+ }
369
+
370
+ // 5. oklch overrides for --ds-gray-*.
371
+ if (oklchMap && oklchMap.size > 0) {
372
+ lines.push(
373
+ `@supports (color: oklch(0 0 0)) {`,
374
+ ` ${selector} {`,
375
+ ` /* oklch values for modern engines (2024+); sRGB hsla() fallback above. */`,
376
+ );
377
+ for (const [name, value] of oklchMap) {
378
+ lines.push(` --${name}: ${value};`);
379
+ }
380
+ lines.push(` }`, `}`, ``);
381
+ }
382
+
383
+ return lines.join("\n");
384
+ }
385
+
386
+ /**
387
+ * Build the Tailwind v4 `@theme inline { ... }` block.
388
+ * Maps semantic CSS variables to Tailwind utility colors (bg-primary, text-foreground, etc.).
389
+ */
390
+ function buildTailwindThemeInline() {
391
+ return `/* ============================================
392
+ Tailwind CSS 4 Theme Configuration (auto-generated)
393
+ ============================================ */
394
+ @theme inline {
395
+ /* Nebutra Brand Color Palette */
396
+ --color-nebutra-blue-50: var(--nebutra-blue-50);
397
+ --color-nebutra-blue-100: var(--nebutra-blue-100);
398
+ --color-nebutra-blue-200: var(--nebutra-blue-200);
399
+ --color-nebutra-blue-300: var(--nebutra-blue-300);
400
+ --color-nebutra-blue-400: var(--nebutra-blue-400);
401
+ --color-nebutra-blue-500: var(--nebutra-blue-500);
402
+ --color-nebutra-blue-600: var(--nebutra-blue-600);
403
+ --color-nebutra-blue-700: var(--nebutra-blue-700);
404
+ --color-nebutra-blue-800: var(--nebutra-blue-800);
405
+ --color-nebutra-blue-900: var(--nebutra-blue-900);
406
+ --color-nebutra-blue-950: var(--nebutra-blue-950);
407
+
408
+ --color-nebutra-cyan-50: var(--nebutra-cyan-50);
409
+ --color-nebutra-cyan-100: var(--nebutra-cyan-100);
410
+ --color-nebutra-cyan-200: var(--nebutra-cyan-200);
411
+ --color-nebutra-cyan-300: var(--nebutra-cyan-300);
412
+ --color-nebutra-cyan-400: var(--nebutra-cyan-400);
413
+ --color-nebutra-cyan-500: var(--nebutra-cyan-500);
414
+ --color-nebutra-cyan-600: var(--nebutra-cyan-600);
415
+ --color-nebutra-cyan-700: var(--nebutra-cyan-700);
416
+ --color-nebutra-cyan-800: var(--nebutra-cyan-800);
417
+ --color-nebutra-cyan-900: var(--nebutra-cyan-900);
418
+ --color-nebutra-cyan-950: var(--nebutra-cyan-950);
419
+
420
+ --color-nebutra-neutral-50: var(--nebutra-neutral-50);
421
+ --color-nebutra-neutral-100: var(--nebutra-neutral-100);
422
+ --color-nebutra-neutral-200: var(--nebutra-neutral-200);
423
+ --color-nebutra-neutral-300: var(--nebutra-neutral-300);
424
+ --color-nebutra-neutral-400: var(--nebutra-neutral-400);
425
+ --color-nebutra-neutral-500: var(--nebutra-neutral-500);
426
+ --color-nebutra-neutral-600: var(--nebutra-neutral-600);
427
+ --color-nebutra-neutral-700: var(--nebutra-neutral-700);
428
+ --color-nebutra-neutral-800: var(--nebutra-neutral-800);
429
+ --color-nebutra-neutral-900: var(--nebutra-neutral-900);
430
+ --color-nebutra-neutral-950: var(--nebutra-neutral-950);
431
+
432
+ /* Semantic Theme Colors */
433
+ --color-background: hsl(var(--background));
434
+ --color-foreground: hsl(var(--foreground));
435
+ --color-card: hsl(var(--card));
436
+ --color-card-foreground: hsl(var(--card-foreground));
437
+ --color-popover: hsl(var(--popover));
438
+ --color-popover-foreground: hsl(var(--popover-foreground));
439
+ --color-primary: hsl(var(--primary));
440
+ --color-primary-foreground: hsl(var(--primary-foreground));
441
+ --color-secondary: hsl(var(--secondary));
442
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
443
+ --color-muted: hsl(var(--muted));
444
+ --color-muted-foreground: hsl(var(--muted-foreground));
445
+ --color-accent: hsl(var(--accent));
446
+ --color-accent-foreground: hsl(var(--accent-foreground));
447
+ --color-destructive: hsl(var(--destructive));
448
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
449
+ --color-success: hsl(var(--success));
450
+ --color-success-foreground: hsl(var(--success-foreground));
451
+ --color-warning: hsl(var(--warning));
452
+ --color-warning-foreground: hsl(var(--warning-foreground));
453
+ --color-info: hsl(var(--info));
454
+ --color-info-foreground: hsl(var(--info-foreground));
455
+ --color-border: hsl(var(--border));
456
+ --color-input: hsl(var(--input));
457
+ --color-ring: hsl(var(--ring));
458
+
459
+ /* Sidebar */
460
+ --color-sidebar: hsl(var(--sidebar));
461
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
462
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
463
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
464
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
465
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
466
+ --color-sidebar-border: hsl(var(--sidebar-border));
467
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
468
+
469
+ /* Brand aliases */
470
+ --color-brand-primary: var(--brand-primary);
471
+ --color-brand-accent: var(--brand-accent);
472
+ --color-brand-tertiary: var(--brand-tertiary);
473
+
474
+ /* Chart colors */
475
+ --color-chart-1: hsl(var(--chart-1));
476
+ --color-chart-2: hsl(var(--chart-2));
477
+ --color-chart-3: hsl(var(--chart-3));
478
+ --color-chart-4: hsl(var(--chart-4));
479
+ --color-chart-5: hsl(var(--chart-5));
480
+
481
+ /* 12-Step Functional Scales */
482
+ --color-neutral-1: var(--neutral-1);
483
+ --color-neutral-2: var(--neutral-2);
484
+ --color-neutral-3: var(--neutral-3);
485
+ --color-neutral-4: var(--neutral-4);
486
+ --color-neutral-5: var(--neutral-5);
487
+ --color-neutral-6: var(--neutral-6);
488
+ --color-neutral-7: var(--neutral-7);
489
+ --color-neutral-8: var(--neutral-8);
490
+ --color-neutral-9: var(--neutral-9);
491
+ --color-neutral-10: var(--neutral-10);
492
+ --color-neutral-11: var(--neutral-11);
493
+ --color-neutral-12: var(--neutral-12);
494
+
495
+ --color-blue-1: var(--blue-1);
496
+ --color-blue-2: var(--blue-2);
497
+ --color-blue-3: var(--blue-3);
498
+ --color-blue-4: var(--blue-4);
499
+ --color-blue-5: var(--blue-5);
500
+ --color-blue-6: var(--blue-6);
501
+ --color-blue-7: var(--blue-7);
502
+ --color-blue-8: var(--blue-8);
503
+ --color-blue-9: var(--blue-9);
504
+ --color-blue-10: var(--blue-10);
505
+ --color-blue-11: var(--blue-11);
506
+ --color-blue-12: var(--blue-12);
507
+
508
+ --color-cyan-1: var(--cyan-1);
509
+ --color-cyan-2: var(--cyan-2);
510
+ --color-cyan-3: var(--cyan-3);
511
+ --color-cyan-4: var(--cyan-4);
512
+ --color-cyan-5: var(--cyan-5);
513
+ --color-cyan-6: var(--cyan-6);
514
+ --color-cyan-7: var(--cyan-7);
515
+ --color-cyan-8: var(--cyan-8);
516
+ --color-cyan-9: var(--cyan-9);
517
+ --color-cyan-10: var(--cyan-10);
518
+ --color-cyan-11: var(--cyan-11);
519
+ --color-cyan-12: var(--cyan-12);
520
+
521
+ /* Geist DS color scale */
522
+ --color-blue-700: var(--ds-blue-700);
523
+ --color-blue-900: var(--ds-blue-900);
524
+ --color-red-200: var(--ds-red-200);
525
+ --color-red-700: var(--ds-red-700);
526
+ --color-red-900: var(--ds-red-900);
527
+ --color-amber-200: var(--ds-amber-200);
528
+ --color-amber-700: var(--ds-amber-700);
529
+ --color-amber-900: var(--ds-amber-900);
530
+ --color-green-200: var(--ds-green-200);
531
+ --color-green-700: var(--ds-green-700);
532
+ --color-green-900: var(--ds-green-900);
533
+ --color-teal-300: var(--ds-teal-300);
534
+ --color-teal-700: var(--ds-teal-700);
535
+ --color-teal-900: var(--ds-teal-900);
536
+ --color-purple-200: var(--ds-purple-200);
537
+ --color-purple-700: var(--ds-purple-700);
538
+ --color-purple-900: var(--ds-purple-900);
539
+ --color-pink-300: var(--ds-pink-300);
540
+ --color-pink-700: var(--ds-pink-700);
541
+ --color-pink-900: var(--ds-pink-900);
542
+ --color-gray-100: var(--ds-gray-100);
543
+ --color-gray-200: var(--ds-gray-200);
544
+ --color-gray-700: var(--ds-gray-700);
545
+ --color-gray-1000: var(--ds-gray-1000);
546
+ --color-trial-start: var(--ds-trial-start);
547
+ --color-trial-end: var(--ds-trial-end);
548
+ --color-turbo-start: var(--ds-turbo-start);
549
+ --color-turbo-end: var(--ds-turbo-end);
550
+ --color-geist-gray-100: var(--ds-gray-100);
551
+ --color-geist-gray-200: var(--ds-gray-200);
552
+ --color-geist-gray-500: var(--ds-gray-500);
553
+ --color-geist-gray-600: var(--ds-gray-600);
554
+ --color-geist-gray-700: var(--ds-gray-700);
555
+ --color-geist-gray-1000: var(--ds-gray-1000);
556
+ --color-geist-background-100: var(--ds-background-100);
557
+
558
+ /* Border Radius */
559
+ --radius-none: 0;
560
+ --radius-sm: 0.25rem;
561
+ --radius-md: 0.375rem;
562
+ --radius-lg: 0.5rem;
563
+ --radius-xl: 0.75rem;
564
+ --radius-2xl: 1rem;
565
+ --radius-3xl: 1.5rem;
566
+ --radius-full: 9999px;
567
+
568
+ /* Shadows */
569
+ --shadow-xs: var(--elevation-xs);
570
+ --shadow-sm: var(--elevation-sm);
571
+ --shadow-md: var(--elevation-md);
572
+ --shadow-lg: var(--elevation-lg);
573
+ --shadow-xl: var(--elevation-xl);
574
+ --shadow-2xl: var(--elevation-2xl);
575
+ --shadow-brand: var(--elevation-brand);
576
+ --shadow-brand-lg: var(--elevation-brand-lg);
577
+
578
+ /* Motion — Easing curves */
579
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
580
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
581
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
582
+ --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
583
+
584
+ /* Motion — Durations (four-rail; see core.json:duration). Names denote intent, not relative speed. */
585
+ --duration-micro: 100ms;
586
+ --duration-flow: 200ms;
587
+ --duration-reveal: 300ms;
588
+ --duration-cinematic: 500ms;
589
+
590
+ /* Font family — Geist primary stack with CJK fallbacks */
591
+ --font-sans: "Geist", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
592
+ --font-cn: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", "vivo Sans", sans-serif;
593
+ --font-display: "Geist", "Noto Sans SC", sans-serif;
594
+ --font-heading: "Geist", "Noto Sans SC", sans-serif;
595
+ --font-mono: "Geist Mono", "Fira Code", ui-monospace, Consolas, "Courier New", monospace;
596
+ }
597
+ `;
598
+ }
599
+
600
+ const p3Map = await buildP3Map();
601
+ const lightOklchMap = await buildOklchMap("tokens/themes/light.json");
602
+ const darkOklchMap = await buildOklchMap("tokens/themes/dark.json");
603
+
604
+ const lightExtras = buildExtras({
605
+ selector: ":root",
606
+ p3Map,
607
+ oklchMap: lightOklchMap,
608
+ includeBrandBridge: true,
609
+ isDark: false,
610
+ });
611
+ const darkExtras = buildExtras({
612
+ selector: ".dark",
613
+ p3Map: null, // P3 only emitted under :root (cascades into .dark)
614
+ oklchMap: darkOklchMap,
615
+ includeBrandBridge: false,
616
+ isDark: true,
617
+ });
618
+
619
+ const lightCss = await readFile("build/css/light.css", "utf8");
620
+ const darkCss = await readFile("build/css/dark.css", "utf8");
621
+
622
+ // Append extras to each mode file.
623
+ const lightFinal = `${lightCss}\n${lightExtras}`;
624
+ const darkFinal = `${darkCss}\n${darkExtras}`;
625
+
626
+ await writeFile("build/css/light.css", lightFinal, "utf8");
627
+ await writeFile("build/css/dark.css", darkFinal, "utf8");
628
+
629
+ const tailwindBlock = buildTailwindThemeInline();
630
+ await writeFile("build/css/theme-inline.css", tailwindBlock, "utf8");
631
+
632
+ // Read the static base CSS (utilities, keyframes, body styles, CJK rules).
633
+ const baseCss = await readFile("static/base.css", "utf8");
634
+
635
+ const generatedHeader = `/* biome-ignore-all lint/suspicious/noDuplicateCustomProperties: auto-generated from W3C DTCG tokens — primitive + semantic blocks intentionally redeclare aliases. Fix in tokens/*.json source if real, then re-run pnpm --filter @nebutra/design-tokens build. */
636
+
637
+ /**
638
+ * @nebutra/design-tokens — styles.generated.css
639
+ * AUTO-GENERATED from packages/design/design-tokens/tokens/*.json — DO NOT EDIT.
640
+ * Run \`pnpm --filter @nebutra/design-tokens build\` after editing tokens.
641
+ *
642
+ * SSOT Architecture:
643
+ * tokens/core.json primitive scales (colors, sizes, durations, fonts)
644
+ * tokens/semantic.json semantic aliases (brand.primary, status.danger, gradients)
645
+ * tokens/themes/light.json 12-step scales + shadcn HSL + Geist DS + elevation
646
+ * tokens/themes/dark.json same shape, dark-mode values
647
+ *
648
+ * Modeling extensions:
649
+ * - Display-P3 wide-gamut overrides (\`@supports (color: color(display-p3 ...))\`)
650
+ * - oklch fallbacks for ds-gray-* (\`@supports (color: oklch(0 0 0))\`)
651
+ * - Composite shorthand tokens (\`--transition\`, \`--focus-ring\`)
652
+ * - Tailwind v4 \`@theme inline\` block
653
+ * - Dark-mode brand alias overrides (\`--brand-primary\` → \`--blue-9\` in .dark)
654
+ */
655
+
656
+ `;
657
+
658
+ // styles.generated.css — used by `pnpm verify:parity` and as the replacement for
659
+ // packages/design/tokens/styles.css. Contains :root + .dark + extras + @theme inline + base.
660
+ const stylesCombined = `${generatedHeader}${lightFinal}\n${darkFinal}\n${tailwindBlock}\n${baseCss}`;
661
+ await writeFile("build/css/styles.generated.css", stylesCombined, "utf8");
662
+
663
+ // themes.generated.css — replacement for packages/design/theme/themes.css.
664
+ // Concatenates all multi-theme files with their data-theme selectors.
665
+ const themeFiles = await Promise.all(
666
+ MULTI_THEMES.map(async (name) => {
667
+ const css = await readFile(`build/css/${name}.css`, "utf8");
668
+ return `/* ─── ${name} theme ─── */\n${css}`;
669
+ }),
670
+ );
671
+ const themesHeader = `/**
672
+ * @nebutra/theme — themes.generated.css
673
+ * AUTO-GENERATED from packages/design/design-tokens/tokens/themes/*.json — DO NOT EDIT.
674
+ *
675
+ * Multi-theme engine — CSS-only:
676
+ * :root neon (default theme)
677
+ * [data-theme="gradient"] Marketing/Growth
678
+ * [data-theme="dark-dense"] DevOps Dashboard
679
+ * [data-theme="minimal"] Blog/Portfolio
680
+ * [data-theme="vibrant"] Creative UI/UX
681
+ * [data-theme="ocean"] Community
682
+ *
683
+ * Integration:
684
+ * 1. Import this file in each app's globals.css AFTER @import "tailwindcss"
685
+ * 2. Configure next-themes with attribute="data-theme" defaultTheme="neon"
686
+ */
687
+
688
+ `;
689
+ const themesBaseCss = await readFile("static/themes-base.css", "utf8");
690
+ const themesCombined = `${themesHeader}${themeFiles.join("\n\n")}\n\n${themesBaseCss}`;
691
+ await writeFile("build/css/themes.generated.css", themesCombined, "utf8");
692
+
693
+ process.stdout.write(
694
+ "\n[design-tokens] build complete\n" +
695
+ " → build/css/styles.generated.css (replaces packages/design/tokens/styles.css)\n" +
696
+ " → build/css/themes.generated.css (replaces packages/design/theme/themes.css)\n",
697
+ );