@motion-proto/live-tokens 0.47.1 → 0.48.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.
Files changed (116) hide show
  1. package/.claude/skills/live-tokens-adjust-shape-space/SKILL.md +68 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-generate-theme/SKILL.md +152 -0
  4. package/CHANGELOG.md +270 -0
  5. package/README.md +37 -26
  6. package/bin/adjust.mjs +254 -0
  7. package/bin/cli.mjs +94 -7
  8. package/bin/generate-theme.mjs +251 -0
  9. package/bin/migrate.mjs +95 -6
  10. package/dist-plugin/adjust/index.cjs +259 -0
  11. package/dist-plugin/adjust/index.d.cts +45 -0
  12. package/dist-plugin/adjust/index.d.ts +45 -0
  13. package/dist-plugin/adjust/index.js +176 -0
  14. package/dist-plugin/{chunk-H4TRUINI.js → chunk-44RSTAII.js} +0 -48
  15. package/dist-plugin/chunk-6OZFXIQI.js +316 -0
  16. package/dist-plugin/chunk-76TFDTJO.js +556 -0
  17. package/dist-plugin/chunk-D3ZVKOR4.js +52 -0
  18. package/dist-plugin/dataPaths-DBN0RPuT.d.cts +54 -0
  19. package/dist-plugin/dataPaths-DBN0RPuT.d.ts +54 -0
  20. package/dist-plugin/generateColorsAndType/index.cjs +1781 -0
  21. package/dist-plugin/generateColorsAndType/index.d.cts +81 -0
  22. package/dist-plugin/generateColorsAndType/index.d.ts +81 -0
  23. package/dist-plugin/generateColorsAndType/index.js +1210 -0
  24. package/dist-plugin/index.cjs +849 -531
  25. package/dist-plugin/index.d.cts +3 -2
  26. package/dist-plugin/index.d.ts +3 -2
  27. package/dist-plugin/index.js +658 -1132
  28. package/dist-plugin/migrateData/index.cjs +728 -0
  29. package/dist-plugin/migrateData/index.d.cts +60 -0
  30. package/dist-plugin/migrateData/index.d.ts +60 -0
  31. package/dist-plugin/migrateData/index.js +348 -0
  32. package/dist-plugin/themeTypes-DMHZOnUn.d.cts +211 -0
  33. package/dist-plugin/themeTypes-DMHZOnUn.d.ts +211 -0
  34. package/dist-plugin/tokensCssMigrations/index.cjs +2 -1
  35. package/dist-plugin/tokensCssMigrations/index.d.cts +3 -15
  36. package/dist-plugin/tokensCssMigrations/index.d.ts +3 -15
  37. package/dist-plugin/tokensCssMigrations/index.js +4 -2
  38. package/package.json +18 -4
  39. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +183 -158
  40. package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +0 -4
  41. package/src/editor/component-editor/scaffolding/SaveAsDialog.svelte +4 -4
  42. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +2 -59
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +1 -1
  44. package/src/editor/core/components/adjustAliases.ts +180 -0
  45. package/src/editor/core/components/aliasKinds.ts +73 -0
  46. package/src/editor/core/components/componentConfigService.ts +26 -38
  47. package/src/editor/core/flashStatus.ts +1 -1
  48. package/src/editor/core/fonts/fontMigration.ts +13 -13
  49. package/src/editor/core/fonts/fontPairing.ts +35 -0
  50. package/src/editor/core/palettes/paletteDerivation.ts +83 -13
  51. package/src/editor/core/preview/lookPreview.ts +141 -0
  52. package/src/editor/core/productionPulse.ts +34 -21
  53. package/src/editor/core/storage/files/versionedFileResourceClient.ts +18 -45
  54. package/src/editor/core/store/editorConfigStore.ts +2 -2
  55. package/src/editor/core/store/editorPersistence.ts +0 -1
  56. package/src/editor/core/store/editorStore.ts +114 -51
  57. package/src/editor/core/store/editorTypes.ts +0 -1
  58. package/src/editor/core/store/gradientSource.ts +2 -2
  59. package/src/editor/core/themes/colorsAndTypeService.ts +95 -0
  60. package/src/editor/core/themes/generateColorsAndType.ts +433 -0
  61. package/src/editor/core/themes/loadRows.ts +64 -0
  62. package/src/editor/core/themes/lookSummary.ts +75 -0
  63. package/src/editor/core/themes/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +3 -3
  64. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -2
  65. package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +3 -3
  66. package/src/editor/core/themes/migrations/2026-08-13-drop-legacy-shape-space-keys.ts +38 -0
  67. package/src/editor/core/themes/migrations/2026-08-15-line-height-scale-rename.ts +57 -0
  68. package/src/editor/core/themes/migrations/index.ts +19 -11
  69. package/src/editor/core/themes/slices/components.ts +4 -4
  70. package/src/editor/core/themes/slices/fonts.ts +1 -1
  71. package/src/editor/core/themes/slices/gradients.ts +22 -0
  72. package/src/editor/core/themes/slices/palettes.ts +2 -2
  73. package/src/editor/core/themes/themeInit.ts +21 -29
  74. package/src/editor/core/themes/themeService.ts +218 -78
  75. package/src/editor/core/themes/themeTypes.ts +94 -51
  76. package/src/editor/docs/Docs.svelte +1 -0
  77. package/src/editor/docs/chapters.ts +1 -0
  78. package/src/editor/docs/content/01-overview.md +1 -1
  79. package/src/editor/docs/content/editing-tokens.md +6 -6
  80. package/src/editor/docs/content/getting-started.md +9 -7
  81. package/src/editor/docs/content/themes-workflow.md +74 -34
  82. package/src/editor/docs/content/where-themes-live.md +61 -0
  83. package/src/editor/docs/content.generated.ts +5 -4
  84. package/src/editor/index.ts +27 -27
  85. package/src/editor/pages/ComponentEditorPage.svelte +2 -2
  86. package/src/editor/pages/EditorShell.svelte +4 -30
  87. package/src/editor/ui/FileLoadList.svelte +67 -7
  88. package/src/editor/ui/GradientEditor.svelte +3 -3
  89. package/src/editor/ui/ThemePanel.svelte +1067 -0
  90. package/src/editor/ui/UIDialog.svelte +11 -9
  91. package/src/editor/ui/index.ts +4 -2
  92. package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
  93. package/src/live-tokens/data/{themes → colors-and-type}/default.json +962 -549
  94. package/src/live-tokens/data/colors-and-type/halloween.json +2529 -0
  95. package/src/live-tokens/data/colors-and-type/midnight-study.json +2536 -0
  96. package/src/live-tokens/data/colors-and-type/ocean.json +2489 -0
  97. package/src/live-tokens/data/colors-and-type/royal-velvet.json +2520 -0
  98. package/src/live-tokens/data/colors-and-type/spring-meadow.json +2476 -0
  99. package/src/live-tokens/data/colors-and-type/sunset.json +2528 -0
  100. package/src/live-tokens/data/themes/autumn.json +3918 -0
  101. package/src/live-tokens/data/themes/halloween.json +3992 -0
  102. package/src/live-tokens/data/themes/midnight-study.json +3932 -0
  103. package/src/live-tokens/data/themes/ocean.json +3917 -0
  104. package/src/live-tokens/data/themes/royal-velvet.json +3983 -0
  105. package/src/live-tokens/data/themes/spring-meadow.json +3904 -0
  106. package/src/live-tokens/data/themes/sunset.json +3931 -0
  107. package/src/live-tokens/data/tokens.generated.css +129 -140
  108. package/src/system/styles/CONVENTIONS.md +1 -1
  109. package/src/system/styles/fonts.css +1 -1
  110. package/template/README.md +7 -5
  111. package/template/_gitignore +0 -6
  112. package/src/editor/core/components/componentPersist.ts +0 -62
  113. package/src/editor/core/manifests/manifestService.ts +0 -172
  114. package/src/editor/ui/ManifestFileManager.svelte +0 -446
  115. package/src/editor/ui/ThemeFileManager.svelte +0 -785
  116. package/src/live-tokens/data/manifests/default.json +0 -35
@@ -1,15 +1,31 @@
1
+ import {
2
+ hexToOklch,
3
+ palettesToVars,
4
+ reconcilePalettesFromCssVars,
5
+ sanitizeFileName
6
+ } from "./chunk-76TFDTJO.js";
7
+ import {
8
+ THEME_SCHEMA_VERSION,
9
+ detectLegacyLayout,
10
+ isColorsAndTypeShaped,
11
+ nextAvailableName,
12
+ normalizeTheme,
13
+ versionedFileResourceServer
14
+ } from "./chunk-6OZFXIQI.js";
1
15
  import {
2
16
  TOKENS_CSS_MIGRATIONS,
3
17
  extractGlobalRootBody,
4
- resolveDataDirs,
5
18
  runAdditiveTokensCssMigrations,
6
19
  runTokensCssMigrations,
7
20
  validateTokensCss
8
- } from "./chunk-H4TRUINI.js";
21
+ } from "./chunk-44RSTAII.js";
22
+ import {
23
+ resolveDataDirs
24
+ } from "./chunk-D3ZVKOR4.js";
9
25
 
10
26
  // vite-plugin/themeFileApi.ts
11
- import fs2 from "fs";
12
- import path2 from "path";
27
+ import fs from "fs";
28
+ import path from "path";
13
29
 
14
30
  // src/editor/core/themes/parsers/colorOpacity.ts
15
31
  var COLOR_OPACITY_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+)%,\s*transparent\)$/i;
@@ -19,509 +35,6 @@ function parseColorOpacity(value) {
19
35
  return { name: m[1], opacity: parseInt(m[2], 10) };
20
36
  }
21
37
 
22
- // src/editor/core/storage/files/versionedFileResourceClient.ts
23
- function sanitizeFileName(name) {
24
- return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9\-_]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unnamed";
25
- }
26
-
27
- // src/editor/core/palettes/oklch.ts
28
- function srgbToLinear(c) {
29
- return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
30
- }
31
- function linearToSrgb(c) {
32
- return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
33
- }
34
- function hexToLinearRgb(hex) {
35
- const r = parseInt(hex.slice(1, 3), 16) / 255;
36
- const g = parseInt(hex.slice(3, 5), 16) / 255;
37
- const b = parseInt(hex.slice(5, 7), 16) / 255;
38
- return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
39
- }
40
- function linearRgbToHex(r, g, b) {
41
- const toHex = (c) => {
42
- const v = Math.round(Math.max(0, Math.min(1, linearToSrgb(c))) * 255);
43
- return v.toString(16).padStart(2, "0");
44
- };
45
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
46
- }
47
- function linearRgbToOklab(r, g, b) {
48
- const l_ = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
49
- const m_ = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
50
- const s_ = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
51
- const l = Math.cbrt(l_);
52
- const m = Math.cbrt(m_);
53
- const s = Math.cbrt(s_);
54
- return [
55
- 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
56
- 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
57
- 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
58
- ];
59
- }
60
- function oklabToLinearRgb(L, a, b) {
61
- const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
62
- const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
63
- const s_ = L - 0.0894841775 * a - 1.291485548 * b;
64
- const l = l_ * l_ * l_;
65
- const m = m_ * m_ * m_;
66
- const s = s_ * s_ * s_;
67
- return [
68
- 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
69
- -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
70
- -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
71
- ];
72
- }
73
- function oklabToOklch(L, a, b) {
74
- const c = Math.sqrt(a * a + b * b);
75
- let h = Math.atan2(b, a) * 180 / Math.PI;
76
- if (h < 0) h += 360;
77
- return { l: L, c, h };
78
- }
79
- function oklchToOklab(l, c, h) {
80
- const hRad = h * Math.PI / 180;
81
- return [l, c * Math.cos(hRad), c * Math.sin(hRad)];
82
- }
83
- function hexToOklch(hex) {
84
- const [r, g, b] = hexToLinearRgb(hex);
85
- const [L, a, bVal] = linearRgbToOklab(r, g, b);
86
- return oklabToOklch(L, a, bVal);
87
- }
88
- function oklchToHex(l, c, h) {
89
- const [L, a, b] = oklchToOklab(l, c, h);
90
- const [r, g, bVal] = oklabToLinearRgb(L, a, b);
91
- return linearRgbToHex(r, g, bVal);
92
- }
93
- function isInGamut(r, g, b) {
94
- const eps = 1e-4;
95
- return r >= -eps && r <= 1 + eps && g >= -eps && g <= 1 + eps && b >= -eps && b <= 1 + eps;
96
- }
97
- function oklchToHexClamped(l, c, h) {
98
- const g = gamutClamp(l, c, h);
99
- return oklchToHex(g.l, g.c, g.h);
100
- }
101
- function gamutClamp(l, c, h) {
102
- if (l <= 0) return { l: 0, c: 0, h };
103
- if (l >= 1) return { l: 1, c: 0, h };
104
- const [L, a, b] = oklchToOklab(l, c, h);
105
- const [r, g, bVal] = oklabToLinearRgb(L, a, b);
106
- if (isInGamut(r, g, bVal)) return { l, c, h };
107
- let lo = 0;
108
- let hi = c;
109
- for (let i = 0; i < 20; i++) {
110
- const mid = (lo + hi) / 2;
111
- const [La, aa, ba] = oklchToOklab(l, mid, h);
112
- const [rr, gg, bb] = oklabToLinearRgb(La, aa, ba);
113
- if (isInGamut(rr, gg, bb)) {
114
- lo = mid;
115
- } else {
116
- hi = mid;
117
- }
118
- }
119
- return { l, c: lo, h };
120
- }
121
-
122
- // src/editor/ui/curveEngine.ts
123
- function makeAnchor(x, y, tangentLen = 15) {
124
- return { x, y, inDx: -tangentLen, inDy: 0, outDx: tangentLen, outDy: 0 };
125
- }
126
- var HANDLE_SPAN = 1 / 3;
127
- function tangentSlope(prev, x, y, next) {
128
- const hPrev = prev ? x - prev.x : 0;
129
- const hNext = next ? next.x - x : 0;
130
- const dPrev = hPrev > 0 ? (y - prev.y) / hPrev : null;
131
- const dNext = hNext > 0 ? (next.y - y) / hNext : null;
132
- if (dPrev === null) return dNext ?? 0;
133
- if (dNext === null) return dPrev;
134
- if (dPrev * dNext <= 0) return 0;
135
- const wPrev = 2 * hNext + hPrev;
136
- const wNext = hNext + 2 * hPrev;
137
- return (wPrev + wNext) / (wPrev / dPrev + wNext / dNext);
138
- }
139
- function tangentAnchor(x, y, prev, next) {
140
- const m = tangentSlope(prev, x, y, next);
141
- const inDx = prev ? -(x - prev.x) * HANDLE_SPAN : 0;
142
- const outDx = next ? (next.x - x) * HANDLE_SPAN : 0;
143
- return { x, y, inDx, inDy: m * inDx, outDx, outDy: m * outDx };
144
- }
145
- function evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, t) {
146
- const u = 1 - t, u2 = u * u, u3 = u2 * u;
147
- const t2 = t * t, t3 = t2 * t;
148
- return {
149
- x: u3 * p0x + 3 * u2 * t * c0x + 3 * u * t2 * c1x + t3 * p1x,
150
- y: u3 * p0y + 3 * u2 * t * c0y + 3 * u * t2 * c1y + t3 * p1y
151
- };
152
- }
153
- function sampleCurve(anchors, xPos) {
154
- if (anchors.length === 0) return 0;
155
- if (anchors.length === 1) return anchors[0].y;
156
- if (xPos <= anchors[0].x) return anchors[0].y;
157
- if (xPos >= anchors[anchors.length - 1].x) return anchors[anchors.length - 1].y;
158
- let seg = 0;
159
- while (seg < anchors.length - 2 && anchors[seg + 1].x < xPos) seg++;
160
- const a0 = anchors[seg], a1 = anchors[seg + 1];
161
- const p0x = a0.x, p0y = a0.y;
162
- const c0x = a0.x + a0.outDx, c0y = a0.y + a0.outDy;
163
- const c1x = a1.x + a1.inDx, c1y = a1.y + a1.inDy;
164
- const p1x = a1.x, p1y = a1.y;
165
- let lo = 0, hi = 1;
166
- for (let i = 0; i < 20; i++) {
167
- const mid = (lo + hi) / 2;
168
- const pt = evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, mid);
169
- if (pt.x < xPos) lo = mid;
170
- else hi = mid;
171
- }
172
- return evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, (lo + hi) / 2).y;
173
- }
174
-
175
- // src/editor/core/palettes/paletteDerivation.ts
176
- var PALETTE_SPECS = [
177
- { label: "Brand", cssNamespace: "brand", initialColor: { l: 0.5573, c: 0.1841, h: 25.49 } },
178
- { label: "Accent", cssNamespace: "accent", initialColor: { l: 0.7674, c: 0.164, h: 70.44 } },
179
- { label: "Special", cssNamespace: "special", initialColor: { l: 0.6056, c: 0.2189, h: 292.72 } },
180
- { label: "Canvas", cssNamespace: "canvas", emptySelector: true, initialColor: { l: 0.2284, c: 0.0384, h: 282.93 } },
181
- { label: "Neutral", cssNamespace: "neutral", neutral: true, initialColor: { l: 0.5679, c: 0.0134, h: 240.07 } },
182
- { label: "Alternate", cssNamespace: "alternate", neutral: true, displayLabel: "Alternate (neutral)", initialColor: { l: 0.5873, c: 87e-4, h: 48.57 } },
183
- { label: "Info", cssNamespace: "info", initialColor: { l: 0.5871, c: 0.1855, h: 259.56 } },
184
- { label: "Success", cssNamespace: "success", initialColor: { l: 0.7198, c: 0.1918, h: 149.52 } },
185
- { label: "Warning", cssNamespace: "warning", initialColor: { l: 0.6704, c: 0.1716, h: 48.75 } },
186
- { label: "Danger", cssNamespace: "danger", initialColor: { l: 0.6103, c: 0.2165, h: 18.1 } }
187
- ];
188
- var PALETTE_STEPS = [
189
- { label: "100" },
190
- { label: "200" },
191
- { label: "300" },
192
- { label: "400" },
193
- { label: "500" },
194
- { label: "600" },
195
- { label: "700" },
196
- { label: "800" },
197
- { label: "850" },
198
- { label: "900" },
199
- { label: "950" }
200
- ];
201
- var SCALES = [
202
- {
203
- title: "Surfaces",
204
- isText: false,
205
- steps: [
206
- { name: "lowest", position: -1 },
207
- { name: "lower", position: -2 / 3 },
208
- { name: "low", position: -1 / 3 },
209
- { name: "default", position: 0 },
210
- { name: "high", position: 1 / 3 },
211
- { name: "higher", position: 2 / 3 },
212
- { name: "highest", position: 1 }
213
- ]
214
- },
215
- {
216
- title: "Borders",
217
- isText: false,
218
- steps: [
219
- { name: "faint", position: -1 },
220
- { name: "subtle", position: -0.5 },
221
- { name: "default", position: 0 },
222
- { name: "medium", position: 0.5 },
223
- { name: "strong", position: 1 }
224
- ]
225
- },
226
- {
227
- title: "Text",
228
- isText: true,
229
- steps: [
230
- { name: "primary", position: 0 },
231
- { name: "secondary", position: 0 },
232
- { name: "tertiary", position: 0 },
233
- { name: "muted", position: 0 },
234
- { name: "disabled", position: 0 }
235
- ]
236
- }
237
- ];
238
- var BW_GUARD_MIN_L = 0.17;
239
- var BW_GUARD_MAX_L = 0.93;
240
- var DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
241
- var DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
242
- function scaleCurveDefaults(scheme = "dark") {
243
- if (scheme === "light") {
244
- return {
245
- Surfaces: {
246
- lightness: () => [makeAnchor(0, 98, 5), makeAnchor(100, 82, 5)],
247
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)]
248
- },
249
- Borders: {
250
- lightness: () => [makeAnchor(0, 92, 5), makeAnchor(100, 45, 5)],
251
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)]
252
- },
253
- Text: {
254
- lightness: () => [makeAnchor(0, 30, 30), makeAnchor(100, 120, 30)],
255
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 15, 30)]
256
- }
257
- };
258
- }
259
- return {
260
- Surfaces: {
261
- lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
262
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)]
263
- },
264
- Borders: {
265
- lightness: () => [makeAnchor(0, 25, 5), makeAnchor(100, 80, 5)],
266
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)]
267
- },
268
- Text: {
269
- lightness: () => [makeAnchor(0, 120, 30), makeAnchor(100, 55, 30)],
270
- saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 15, 30)]
271
- }
272
- };
273
- }
274
- var defaultScaleCurves = scaleCurveDefaults("dark");
275
- function paletteStepKey(label) {
276
- return `Palette-${label}`;
277
- }
278
- function stepKey(scaleTitle, stepName) {
279
- return `${scaleTitle}-${stepName}`;
280
- }
281
- function stepIndexToX(index) {
282
- return index / (PALETTE_STEPS.length - 1) * 100;
283
- }
284
- function scaleStepToX(step, scale) {
285
- const idx = scale.steps.indexOf(step);
286
- return scale.steps.length > 1 ? idx / (scale.steps.length - 1) * 100 : 50;
287
- }
288
- function serializeDerivedValue(value) {
289
- return value.kind === "raw" ? value.css : oklchToHexClamped(value.l, value.c, value.h);
290
- }
291
- function computePaletteOklch(index, base, lightnessCurve, saturationCurve, curveOffset) {
292
- const { c: baseC, h } = base;
293
- const xPos = stepIndexToX(index);
294
- const targetL = Math.max(0, Math.min(100, sampleCurve(lightnessCurve, xPos) + (curveOffset.lightness ?? 0))) / 100;
295
- const satMul = Math.max(0, Math.min(2, (sampleCurve(saturationCurve, xPos) + (curveOffset.saturation ?? 0)) / 100));
296
- const targetC = baseC * satMul;
297
- return gamutClamp(targetL, targetC, h);
298
- }
299
- function nearestPaletteStep(lightnessCurve, baseL) {
300
- let best = 0;
301
- let bestD = Infinity;
302
- for (let i = 0; i < PALETTE_STEPS.length; i++) {
303
- const d = Math.abs(sampleCurve(lightnessCurve, stepIndexToX(i)) - baseL * 100);
304
- if (d < bestD) {
305
- bestD = d;
306
- best = i;
307
- }
308
- }
309
- return best;
310
- }
311
- function setCurveAnchor(curve, x, y) {
312
- const existing = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
313
- if (existing >= 0) {
314
- const displacedY = curve[existing].y;
315
- return { curve: curve.map((a, i) => i === existing ? { ...a, x, y } : a), displacedY };
316
- }
317
- let insertAt = curve.findIndex((a) => a.x > x);
318
- if (insertAt < 0) insertAt = curve.length;
319
- const prev = curve[insertAt - 1] ?? null;
320
- const next = curve[insertAt] ?? null;
321
- const out = [...curve];
322
- if (prev && next) {
323
- const span = next.x - prev.x;
324
- out[insertAt - 1] = scaleOutHandle(prev, (x - prev.x) / span);
325
- out[insertAt] = scaleInHandle(next, (next.x - x) / span);
326
- }
327
- out.splice(insertAt, 0, fitBetween(tangentAnchor(x, y, prev, next), out[insertAt - 1] ?? null, out[insertAt] ?? null));
328
- return { curve: out };
329
- }
330
- var scaleOutHandle = (a, k) => ({ ...a, outDx: a.outDx * k, outDy: a.outDy * k });
331
- var scaleInHandle = (a, k) => ({ ...a, inDx: a.inDx * k, inDy: a.inDy * k });
332
- function fitBetween(a, prev, next) {
333
- let out = a;
334
- if (prev) {
335
- const room = a.x - (prev.x + prev.outDx);
336
- if (room < -out.inDx) out = scaleInHandle(out, room <= 0 ? 0 : room / -out.inDx);
337
- }
338
- if (next) {
339
- const room = next.x + next.inDx - a.x;
340
- if (room < out.outDx) out = scaleOutHandle(out, room <= 0 ? 0 : room / out.outDx);
341
- }
342
- return out;
343
- }
344
- function liftCurveAnchor(curve, x, displacedY) {
345
- const idx = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
346
- if (idx < 0) return curve;
347
- if (displacedY !== void 0) return curve.map((a, i) => i === idx ? { ...a, y: displacedY } : a);
348
- if (idx === 0 || idx === curve.length - 1) return curve;
349
- const out = curve.filter((_, i) => i !== idx);
350
- const prev = curve[idx - 1];
351
- const next = curve[idx + 1];
352
- if (prev && next) {
353
- const span = next.x - prev.x;
354
- out[idx - 1] = scaleOutHandle(prev, span / (curve[idx].x - prev.x));
355
- out[idx] = scaleInHandle(next, span / (next.x - curve[idx].x));
356
- }
357
- return out;
358
- }
359
- function syncBaseAnchor(cfg) {
360
- if (cfg.anchorToBase === false) return;
361
- const prev = cfg.anchorPlacement;
362
- let lCurve = cfg.lightnessCurve;
363
- let sCurve = cfg.saturationCurve;
364
- if (prev) {
365
- const prevX = stepIndexToX(prev.step);
366
- lCurve = liftCurveAnchor(lCurve, prevX, prev.displacedL);
367
- sCurve = liftCurveAnchor(sCurve, prevX, prev.displacedS);
368
- }
369
- const step = nearestPaletteStep(lCurve, cfg.baseColor.l);
370
- const x = stepIndexToX(step);
371
- const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100);
372
- const s = setCurveAnchor(sCurve, x, 100);
373
- cfg.lightnessCurve = l.curve;
374
- cfg.saturationCurve = s.curve;
375
- cfg.anchorPlacement = { step, displacedL: l.displacedY, displacedS: s.displacedY };
376
- }
377
- function computeDerivedOklch(step, base, scaleTitle, scaleCurves, curveOffset) {
378
- const scale = SCALES.find((s) => s.title === scaleTitle);
379
- const xPos = scaleStepToX(step, scale);
380
- const defs = defaultScaleCurves[scaleTitle];
381
- const lCurve = scaleCurves[scaleTitle]?.lightness ?? defs.lightness();
382
- const sCurve = scaleCurves[scaleTitle]?.saturation ?? defs.saturation();
383
- const lOff = curveOffset[`${scaleTitle}-lightness`] ?? 0;
384
- const sOff = curveOffset[`${scaleTitle}-saturation`] ?? 0;
385
- const { l: baseL, c: baseC, h: baseH } = base;
386
- let targetL;
387
- if (scale.isText) {
388
- const lMul = Math.max(0, Math.min(2, (sampleCurve(lCurve, xPos) + lOff) / 100));
389
- targetL = Math.max(0, Math.min(1, baseL * lMul));
390
- } else {
391
- targetL = Math.max(0, Math.min(100, sampleCurve(lCurve, xPos) + lOff)) / 100;
392
- }
393
- const satMul = Math.max(0, Math.min(2, (sampleCurve(sCurve, xPos) + sOff) / 100));
394
- const targetC = baseC * satMul;
395
- return gamutClamp(targetL, targetC, baseH);
396
- }
397
- function scaleToCssVar(scaleTitle, stepName, cssNamespace) {
398
- if (cssNamespace === null) return null;
399
- if (scaleTitle === "Surfaces") {
400
- const suffix = stepName === "default" ? "" : `-${stepName}`;
401
- return cssNamespace === "neutral" ? `--surface-neutral${suffix}` : `--surface-${cssNamespace}${suffix}`;
402
- }
403
- if (scaleTitle === "Borders") {
404
- const suffix = stepName === "default" ? "" : `-${stepName}`;
405
- return cssNamespace === "neutral" ? `--border-neutral${suffix}` : `--border-${cssNamespace}${suffix}`;
406
- }
407
- if (scaleTitle === "Text") {
408
- if (cssNamespace === "neutral") return `--text-${stepName}`;
409
- return stepName === "primary" ? `--text-${cssNamespace}` : `--text-${cssNamespace}-${stepName}`;
410
- }
411
- return null;
412
- }
413
- function derivePaletteValues(spec, config) {
414
- const out = {};
415
- if (!config) return out;
416
- const baseColor = config.baseColor ?? spec.initialColor;
417
- const overrides = config.overrides ?? {};
418
- const curveOffset = config.curveOffset ?? {};
419
- const scaleCurves = config.scaleCurves ?? {};
420
- const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
421
- const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
422
- PALETTE_STEPS.forEach((ps, index) => {
423
- const k = paletteStepKey(ps.label);
424
- const value = k in overrides ? { kind: "color", ...overrides[k] } : { kind: "color", ...computePaletteOklch(index, baseColor, lightnessCurve, saturationCurve, curveOffset) };
425
- out[`--color-${spec.cssNamespace}-${ps.label}`] = value;
426
- });
427
- for (const scale of SCALES) {
428
- for (const step of scale.steps) {
429
- const k = stepKey(scale.title, step.name);
430
- const value = k in overrides ? { kind: "color", ...overrides[k] } : { kind: "color", ...computeDerivedOklch(step, baseColor, scale.title, scaleCurves, curveOffset) };
431
- const varName = scaleToCssVar(scale.title, step.name, spec.cssNamespace);
432
- if (varName) out[varName] = value;
433
- }
434
- }
435
- const textPrimary = out["--text-primary"];
436
- if (textPrimary?.kind === "color") {
437
- const mirroredL = Math.max(BW_GUARD_MIN_L, Math.min(BW_GUARD_MAX_L, 1 - textPrimary.l));
438
- out["--text-inverted"] = { kind: "color", ...gamutClamp(mirroredL, textPrimary.c, textPrimary.h) };
439
- }
440
- if (spec.emptySelector) {
441
- const emptyMode = config.emptyMode ?? "solid";
442
- const gradientStyle = config.gradientStyle ?? "linear";
443
- const gradientAngle = config.gradientAngle ?? 180;
444
- const gradientReverse = config.gradientReverse ?? false;
445
- const gradientSize = config.gradientSize ?? "page";
446
- const gradientStops = config.gradientStops ?? [
447
- { position: 0, paletteLabel: "800" },
448
- { position: 100, paletteLabel: "950" }
449
- ];
450
- if (emptyMode === "solid") {
451
- out["--page-bg"] = { kind: "color", ...baseColor };
452
- out["--page-bg-attachment"] = { kind: "raw", css: "scroll" };
453
- } else {
454
- const sortedStops = [...gradientStops].sort(
455
- (a, b) => gradientReverse ? b.position - a.position : a.position - b.position
456
- );
457
- const stopsCss = sortedStops.map((s) => {
458
- const stopValue = out[`--color-${spec.cssNamespace}-${s.paletteLabel}`];
459
- const hex = stopValue ? serializeDerivedValue(stopValue) : "#000000";
460
- return `${hex} ${s.position}%`;
461
- }).join(", ");
462
- let gradient;
463
- switch (gradientStyle) {
464
- case "radial":
465
- gradient = `radial-gradient(circle, ${stopsCss})`;
466
- break;
467
- case "conic":
468
- gradient = `conic-gradient(from ${gradientAngle}deg, ${stopsCss})`;
469
- break;
470
- default:
471
- gradient = `linear-gradient(${gradientAngle}deg, ${stopsCss})`;
472
- }
473
- out["--page-bg"] = { kind: "raw", css: gradient };
474
- out["--page-bg-attachment"] = { kind: "raw", css: gradientSize === "window" ? "fixed" : "scroll" };
475
- }
476
- }
477
- return out;
478
- }
479
- function derivePaletteVars(spec, config) {
480
- const out = {};
481
- for (const [k, v] of Object.entries(derivePaletteValues(spec, config))) {
482
- out[k] = serializeDerivedValue(v);
483
- }
484
- return out;
485
- }
486
- function palettesToValues(palettes) {
487
- const out = {};
488
- for (const spec of PALETTE_SPECS) {
489
- Object.assign(out, derivePaletteValues(spec, palettes[spec.label]));
490
- }
491
- return out;
492
- }
493
- function palettesToVars(palettes) {
494
- const out = {};
495
- for (const [k, v] of Object.entries(palettesToValues(palettes))) {
496
- out[k] = serializeDerivedValue(v);
497
- }
498
- return out;
499
- }
500
- var HEX_RE = /^#[0-9a-f]{6}$/i;
501
- function reconcilePalettesFromCssVars(palettes, cssVars) {
502
- const next = structuredClone(palettes);
503
- const consumed = /* @__PURE__ */ new Set();
504
- const snapped = /* @__PURE__ */ new Set();
505
- for (const spec of PALETTE_SPECS) {
506
- const current = next[spec.label];
507
- if (current === void 0) continue;
508
- if (current._imported === true) {
509
- const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
510
- if (anchorHex && HEX_RE.test(anchorHex.trim())) {
511
- next[spec.label] = { ...current, baseColor: hexToOklch(anchorHex.trim()), _imported: false };
512
- syncBaseAnchor(next[spec.label]);
513
- snapped.add(spec.label);
514
- } else {
515
- next[spec.label] = { ...current, _imported: false };
516
- }
517
- }
518
- for (const k of Object.keys(derivePaletteVars(spec, next[spec.label]))) {
519
- consumed.add(k);
520
- }
521
- }
522
- return { palettes: next, consumed, snapped };
523
- }
524
-
525
38
  // src/editor/core/themes/migrations/2026-07-21-palette-oklch-basis.ts
526
39
  var round = (v, dp) => {
527
40
  const f = 10 ** dp;
@@ -542,102 +55,6 @@ function migratePaletteColorsToOklch(editorConfigs) {
542
55
  return out;
543
56
  }
544
57
 
545
- // vite-plugin/files/versionedFileResourceServer.ts
546
- import fs from "fs";
547
- import path from "path";
548
- function versionedFileResourceServer(opts) {
549
- const dir = opts.dir;
550
- const activePath = path.join(dir, "_active.json");
551
- const productionPath = path.join(dir, "_production.json");
552
- const defaultName = opts.defaultName ?? "default";
553
- const resolvedDir = path.resolve(dir);
554
- const resolvedPackageDir = opts.packageDir ? path.resolve(opts.packageDir) : null;
555
- function ensureDir() {
556
- if (!fs.existsSync(dir)) {
557
- fs.mkdirSync(dir, { recursive: true });
558
- }
559
- }
560
- function ensureMeta() {
561
- if (!fs.existsSync(activePath)) {
562
- fs.writeFileSync(activePath, JSON.stringify({ activeFile: defaultName }));
563
- }
564
- if (!fs.existsSync(productionPath)) {
565
- fs.writeFileSync(productionPath, JSON.stringify({ productionFile: defaultName }));
566
- }
567
- }
568
- function filePath(name) {
569
- return path.join(dir, `${name}.json`);
570
- }
571
- function existingPath(name) {
572
- const local = filePath(name);
573
- if (fs.existsSync(local)) return local;
574
- if (resolvedPackageDir) {
575
- const pkg = path.join(resolvedPackageDir, `${name}.json`);
576
- if (fs.existsSync(pkg)) return pkg;
577
- }
578
- return null;
579
- }
580
- function readJson(name) {
581
- const resolved = existingPath(name);
582
- if (!resolved) return null;
583
- return JSON.parse(fs.readFileSync(resolved, "utf-8"));
584
- }
585
- function listNames() {
586
- const names = [];
587
- const seen = /* @__PURE__ */ new Set();
588
- const collect = (d) => {
589
- if (!d || !fs.existsSync(d)) return;
590
- for (const f of fs.readdirSync(d)) {
591
- if (!f.endsWith(".json") || f.startsWith("_")) continue;
592
- const name = f.slice(0, -".json".length);
593
- if (seen.has(name)) continue;
594
- seen.add(name);
595
- names.push(name);
596
- }
597
- };
598
- collect(resolvedDir);
599
- if (resolvedPackageDir && resolvedPackageDir !== resolvedDir) collect(resolvedPackageDir);
600
- return names;
601
- }
602
- function getActiveName() {
603
- try {
604
- const data = JSON.parse(fs.readFileSync(activePath, "utf-8"));
605
- return data.activeFile || defaultName;
606
- } catch {
607
- return defaultName;
608
- }
609
- }
610
- function getProductionName() {
611
- try {
612
- const data = JSON.parse(fs.readFileSync(productionPath, "utf-8"));
613
- return data.productionFile || defaultName;
614
- } catch {
615
- return defaultName;
616
- }
617
- }
618
- function setActiveName(name) {
619
- fs.writeFileSync(activePath, JSON.stringify({ activeFile: name }));
620
- }
621
- function setProductionName(name) {
622
- fs.writeFileSync(productionPath, JSON.stringify({ productionFile: name }));
623
- }
624
- return {
625
- dir,
626
- activePath,
627
- productionPath,
628
- ensureDir,
629
- ensureMeta,
630
- filePath,
631
- existingPath,
632
- readJson,
633
- listNames,
634
- getActiveName,
635
- getProductionName,
636
- setActiveName,
637
- setProductionName
638
- };
639
- }
640
-
641
58
  // vite-plugin/files/routeTable.ts
642
59
  async function dispatch(req, res, routes) {
643
60
  const url = req.url || "";
@@ -666,28 +83,18 @@ async function dispatch(req, res, routes) {
666
83
  return false;
667
84
  }
668
85
 
669
- // vite-plugin/files/nameAllocator.ts
670
- function nextAvailableName(exists, baseName, maxAttempts = 1e3) {
671
- if (!exists(baseName)) return baseName;
672
- for (let i = 2; i < maxAttempts; i++) {
673
- const candidate = `${baseName}-${i}`;
674
- if (!exists(candidate)) return candidate;
675
- }
676
- throw new Error(`Could not allocate a non-colliding name for "${baseName}"`);
677
- }
678
-
679
86
  // vite-plugin/themeFileApi.ts
680
87
  import { fileURLToPath } from "url";
681
88
  var PKG_VERSION = (() => {
682
89
  try {
683
- let dir = path2.dirname(fileURLToPath(import.meta.url));
90
+ let dir = path.dirname(fileURLToPath(import.meta.url));
684
91
  for (let i = 0; i < 4; i++) {
685
- const p = path2.join(dir, "package.json");
686
- if (fs2.existsSync(p)) {
687
- const json = JSON.parse(fs2.readFileSync(p, "utf-8"));
92
+ const p = path.join(dir, "package.json");
93
+ if (fs.existsSync(p)) {
94
+ const json = JSON.parse(fs.readFileSync(p, "utf-8"));
688
95
  if (json?.name === "@motion-proto/live-tokens") return json.version ?? "";
689
96
  }
690
- const up = path2.dirname(dir);
97
+ const up = path.dirname(dir);
691
98
  if (up === dir) break;
692
99
  dir = up;
693
100
  }
@@ -695,65 +102,79 @@ var PKG_VERSION = (() => {
695
102
  }
696
103
  return "";
697
104
  })();
105
+ var BUNDLE_KIND_BY_VERSION = {
106
+ 1: "manifest-bundle",
107
+ [THEME_SCHEMA_VERSION]: "theme-bundle"
108
+ };
698
109
  function themeFileApi(opts) {
699
110
  const dataDirs = resolveDataDirs({
700
111
  dataDir: opts.dataDir,
701
- themesDir: opts.themesDir,
112
+ colorsAndTypeDir: opts.colorsAndTypeDir,
702
113
  componentConfigsDir: opts.componentConfigsDir,
703
- manifestsDir: opts.manifestsDir
114
+ themesDir: opts.themesDir
704
115
  });
705
- const THEMES_DIR = dataDirs.themesDir;
116
+ const COLORS_AND_TYPE_DIR = dataDirs.colorsAndTypeDir;
706
117
  const COMPONENT_CONFIGS_DIR = dataDirs.componentConfigsDir;
707
- const MANIFESTS_DIR = dataDirs.manifestsDir;
708
- const CSS_PATH = path2.resolve(opts.tokensCssPath);
709
- const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? path2.resolve(opts.tokensGeneratedCssPath) : path2.join(dataDirs.dataDir, "tokens.generated.css");
710
- const FONTS_CSS_PATH = opts.fontsCssPath ? path2.resolve(opts.fontsCssPath) : path2.join(path2.dirname(CSS_PATH), "fonts.css");
118
+ const THEMES_DIR = dataDirs.themesDir;
119
+ const CSS_PATH = path.resolve(opts.tokensCssPath);
120
+ const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? path.resolve(opts.tokensGeneratedCssPath) : path.join(dataDirs.dataDir, "tokens.generated.css");
121
+ const FONTS_CSS_PATH = opts.fontsCssPath ? path.resolve(opts.fontsCssPath) : path.join(path.dirname(CSS_PATH), "fonts.css");
711
122
  const API_BASE = opts.apiBase ?? "/api/live-tokens";
712
- const consumerComponentDirs = opts.componentsSrcDir ? [path2.resolve(opts.componentsSrcDir)] : [path2.resolve("src/components"), path2.resolve("src/system/components")];
713
- const packageComponentsDir = path2.resolve(
714
- path2.dirname(fileURLToPath(import.meta.url)),
123
+ const consumerComponentDirs = opts.componentsSrcDir ? [path.resolve(opts.componentsSrcDir)] : [path.resolve("src/components"), path.resolve("src/system/components")];
124
+ const packageComponentsDir = path.resolve(
125
+ path.dirname(fileURLToPath(import.meta.url)),
715
126
  "..",
716
127
  "src",
717
128
  "system",
718
129
  "components"
719
130
  );
720
- const packageDataDir = path2.resolve(
721
- path2.dirname(fileURLToPath(import.meta.url)),
131
+ const packageDataDir = path.resolve(
132
+ path.dirname(fileURLToPath(import.meta.url)),
722
133
  "..",
723
134
  "src",
724
135
  "live-tokens",
725
136
  "data"
726
137
  );
727
- const packageThemesDir = path2.join(packageDataDir, "themes");
728
- const packageManifestsDir = path2.join(packageDataDir, "manifests");
729
- const packageComponentConfigsDir = path2.join(packageDataDir, "component-configs");
138
+ const packageColorsAndTypeDir = path.join(packageDataDir, "colors-and-type");
139
+ const packageThemesDir = path.join(packageDataDir, "themes");
140
+ const packageComponentConfigsDir = path.join(packageDataDir, "component-configs");
141
+ function shippedColorsAndTypeNames(dataDir) {
142
+ try {
143
+ const pkgRoot = path.resolve(dataDir, "..", "..", "..");
144
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf-8"));
145
+ const files = Array.isArray(pkg.files) ? pkg.files : [];
146
+ return files.filter((f) => /^src\/live-tokens\/data\/colors-and-type\/[^/]+\.json$/.test(f)).map((f) => path.basename(f, ".json"));
147
+ } catch {
148
+ return [];
149
+ }
150
+ }
730
151
  const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
731
- if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && fs2.existsSync(packageComponentsDir)) {
152
+ if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && fs.existsSync(packageComponentsDir)) {
732
153
  COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
733
154
  }
734
- const themesResource = versionedFileResourceServer({
735
- dir: THEMES_DIR,
736
- packageDir: packageThemesDir
155
+ const colorsAndTypeResource = versionedFileResourceServer({
156
+ dir: COLORS_AND_TYPE_DIR,
157
+ packageDir: packageColorsAndTypeDir,
158
+ packageOwnedNames: shippedColorsAndTypeNames(packageDataDir)
737
159
  });
738
160
  const componentResourceCache = /* @__PURE__ */ new Map();
739
161
  function componentResource(comp) {
740
162
  let r = componentResourceCache.get(comp);
741
163
  if (!r) {
742
164
  r = versionedFileResourceServer({
743
- dir: path2.join(COMPONENT_CONFIGS_DIR, comp),
744
- packageDir: path2.join(packageComponentConfigsDir, comp)
165
+ dir: path.join(COMPONENT_CONFIGS_DIR, comp),
166
+ packageDir: path.join(packageComponentConfigsDir, comp)
745
167
  });
746
168
  componentResourceCache.set(comp, r);
747
169
  }
748
170
  return r;
749
171
  }
750
- const manifestsResource = versionedFileResourceServer({
751
- dir: MANIFESTS_DIR,
752
- packageDir: packageManifestsDir
172
+ const themesResource = versionedFileResourceServer({
173
+ dir: THEMES_DIR,
174
+ packageDir: packageThemesDir
753
175
  });
754
- function ensureThemesDir() {
755
- themesResource.ensureDir();
756
- themesResource.ensureMeta();
176
+ function ensureColorsAndTypeDir() {
177
+ colorsAndTypeResource.ensureDir();
757
178
  }
758
179
  function readBody(req) {
759
180
  return new Promise((resolve, reject) => {
@@ -768,25 +189,33 @@ function themeFileApi(opts) {
768
189
  res.setHeader("Content-Type", "application/json");
769
190
  res.end(JSON.stringify(data));
770
191
  }
771
- function normalizeTheme(theme) {
772
- if (!theme || typeof theme !== "object") return theme;
773
- const palettes = theme.editorConfigs ?? {};
774
- const cssVars = theme.cssVariables ?? {};
192
+ function rejectReservedName(res, name) {
193
+ if (!name.startsWith("_")) return false;
194
+ jsonResponse(res, 400, {
195
+ error: 'Names starting with "_" are reserved',
196
+ code: "RESERVED_NAME"
197
+ });
198
+ return true;
199
+ }
200
+ function normalizeColorsAndType(colorsAndType) {
201
+ if (!colorsAndType || typeof colorsAndType !== "object") return colorsAndType;
202
+ const palettes = colorsAndType.editorConfigs ?? {};
203
+ const cssVars = colorsAndType.cssVariables ?? {};
775
204
  const { palettes: nextPalettes, consumed } = reconcilePalettesFromCssVars(palettes, cssVars);
776
205
  const nextCssVars = {};
777
206
  for (const [k, v] of Object.entries(cssVars)) {
778
207
  if (!consumed.has(k)) nextCssVars[k] = v;
779
208
  }
780
- return { ...theme, editorConfigs: nextPalettes, cssVariables: nextCssVars };
209
+ return { ...colorsAndType, editorConfigs: nextPalettes, cssVariables: nextCssVars };
781
210
  }
782
211
  const SYSTEM_CASCADES_SSR = {
783
212
  "system-ui-sans": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
784
213
  "system-ui-serif": 'Georgia, "Times New Roman", serif',
785
214
  "system-ui-mono": 'ui-monospace, "SF Mono", Menlo, Consolas, monospace'
786
215
  };
787
- function resolveFontStacks(themeData) {
788
- const stacks = themeData.fontStacks;
789
- const sources = themeData.fontSources;
216
+ function resolveFontStacks(colorsAndTypeData) {
217
+ const stacks = colorsAndTypeData.fontStacks;
218
+ const sources = colorsAndTypeData.fontSources;
790
219
  if (!stacks || stacks.length === 0) return {};
791
220
  const familyById = /* @__PURE__ */ new Map();
792
221
  for (const src of sources ?? []) {
@@ -812,21 +241,28 @@ function themeFileApi(opts) {
812
241
  }
813
242
  function regenerateTokensCss() {
814
243
  const lines = [];
815
- lines.push("/* Generated by themeFileApi from the production theme and component configs. Do not edit. */");
244
+ lines.push("/* Generated by themeFileApi from the production theme. Do not edit. */");
816
245
  lines.push("/* tokens.css holds developer-authored defaults; this file holds editor overrides. */");
817
246
  lines.push("");
818
247
  const productionThemeName = themesResource.getProductionName();
819
- const themeData = themesResource.readJson(productionThemeName);
820
- let themeVarCount = 0;
821
- if (themeData) {
822
- const cssVars = { ...themeData.cssVariables || {} };
823
- Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(themeData.editorConfigs ?? {})));
824
- const resolvedFontVars = resolveFontStacks(themeData);
248
+ const productionTheme = readTheme(productionThemeName)?.theme ?? null;
249
+ if (themesResource.existingPath(productionThemeName) !== null && !productionTheme?.colorsAndType) {
250
+ const tail = `${path.basename(GENERATED_CSS_PATH)} was left as it was.`;
251
+ throw new Error(
252
+ unreadableThemeReason(productionThemeName) === "colors-and-type" ? `[live-tokens] ${notAThemeError(productionThemeName)} ${tail}` : `[live-tokens] Production theme "${productionThemeName}" is unreadable, so ${tail} Repair the theme file or adopt another theme.`
253
+ );
254
+ }
255
+ const colorsAndTypeData = productionTheme?.colorsAndType;
256
+ let colorsAndTypeVarCount = 0;
257
+ if (colorsAndTypeData) {
258
+ const cssVars = { ...colorsAndTypeData.cssVariables || {} };
259
+ Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(colorsAndTypeData.editorConfigs ?? {})));
260
+ const resolvedFontVars = resolveFontStacks(colorsAndTypeData);
825
261
  for (const [name, value] of Object.entries(resolvedFontVars)) {
826
262
  cssVars[name] = value;
827
263
  }
828
- themeVarCount = Object.keys(cssVars).length;
829
- if (themeVarCount > 0) {
264
+ colorsAndTypeVarCount = Object.keys(cssVars).length;
265
+ if (colorsAndTypeVarCount > 0) {
830
266
  lines.push(`/* Production theme: ${productionThemeName} */`);
831
267
  lines.push(":root:root {");
832
268
  for (const [name, value] of Object.entries(cssVars)) {
@@ -837,12 +273,10 @@ function themeFileApi(opts) {
837
273
  }
838
274
  }
839
275
  let componentOverrideCount = 0;
840
- if (fs2.existsSync(COMPONENT_CONFIGS_DIR)) {
276
+ if (fs.existsSync(COMPONENT_CONFIGS_DIR)) {
841
277
  const blocks = [];
842
278
  for (const comp of listComponentNames()) {
843
- const prod = componentResource(comp).getProductionName();
844
- if (prod === "default") continue;
845
- const prodCfg = readComponentConfig(comp, prod);
279
+ const prodCfg = productionTheme?.componentConfigs[comp];
846
280
  const defaultCfg = readComponentConfig(comp, "default");
847
281
  if (!prodCfg || !defaultCfg) continue;
848
282
  const overrides = [];
@@ -853,7 +287,7 @@ function themeFileApi(opts) {
853
287
  }
854
288
  }
855
289
  if (overrides.length === 0) continue;
856
- const block = [` /* ${comp} (${prod}) */`];
290
+ const block = [` /* ${comp} */`];
857
291
  for (const [varName, semanticValue] of overrides) {
858
292
  block.push(` ${varName}: ${aliasValueToCss(semanticValue)};`);
859
293
  }
@@ -871,24 +305,20 @@ function themeFileApi(opts) {
871
305
  lines.push("");
872
306
  }
873
307
  }
874
- if (!fs2.existsSync(path2.dirname(GENERATED_CSS_PATH))) {
875
- fs2.mkdirSync(path2.dirname(GENERATED_CSS_PATH), { recursive: true });
308
+ if (!fs.existsSync(path.dirname(GENERATED_CSS_PATH))) {
309
+ fs.mkdirSync(path.dirname(GENERATED_CSS_PATH), { recursive: true });
876
310
  }
877
- fs2.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
311
+ fs.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
878
312
  console.log(
879
- `[regenerateTokensCss] Wrote ${path2.basename(GENERATED_CSS_PATH)} (${themeVarCount} theme vars, ${componentOverrideCount} component overrides)`
313
+ `[regenerateTokensCss] Wrote ${path.basename(GENERATED_CSS_PATH)} (${colorsAndTypeVarCount} theme vars, ${componentOverrideCount} component overrides)`
880
314
  );
881
315
  }
882
- function syncTokensToCss(_fileName) {
883
- regenerateTokensCss();
884
- }
885
- function syncFontsToCss(fileName) {
886
- const themeData = themesResource.readJson(fileName);
887
- if (!themeData) return;
888
- const sources = themeData.fontSources;
316
+ function syncFontsToCss(colorsAndTypeData) {
317
+ if (!colorsAndTypeData) return;
318
+ const sources = colorsAndTypeData.fontSources;
889
319
  if (!sources) return;
890
320
  const lines = [];
891
- lines.push("/* Generated from the production theme by syncFontsToCss. Do not edit. */");
321
+ lines.push("/* Generated from the production colors and type by syncFontsToCss. Do not edit. */");
892
322
  lines.push("/* Both fonts.css and fonts/ are in dist/, so relative paths work at runtime. */");
893
323
  lines.push("");
894
324
  const urlSources = sources.filter((s) => s.kind !== "font-face" && s.url);
@@ -908,11 +338,11 @@ function themeFileApi(opts) {
908
338
  lines.push("");
909
339
  }
910
340
  const content = lines.join("\n");
911
- if (!fs2.existsSync(path2.dirname(FONTS_CSS_PATH))) {
912
- fs2.mkdirSync(path2.dirname(FONTS_CSS_PATH), { recursive: true });
341
+ if (!fs.existsSync(path.dirname(FONTS_CSS_PATH))) {
342
+ fs.mkdirSync(path.dirname(FONTS_CSS_PATH), { recursive: true });
913
343
  }
914
- fs2.writeFileSync(FONTS_CSS_PATH, content);
915
- console.log(`[syncFontsToCss] Wrote ${sources.length} source(s) into ${path2.basename(FONTS_CSS_PATH)}`);
344
+ fs.writeFileSync(FONTS_CSS_PATH, content);
345
+ console.log(`[syncFontsToCss] Wrote ${sources.length} source(s) into ${path.basename(FONTS_CSS_PATH)}`);
916
346
  }
917
347
  function extractAliasDeclarations(body) {
918
348
  const aliases = {};
@@ -927,11 +357,11 @@ function themeFileApi(opts) {
927
357
  return aliases;
928
358
  }
929
359
  function componentNameFromFile(filePath) {
930
- return path2.basename(filePath, ".svelte").toLowerCase();
360
+ return path.basename(filePath, ".svelte").toLowerCase();
931
361
  }
932
362
  function isThemeAwareComponent(filePath) {
933
363
  try {
934
- const source = fs2.readFileSync(filePath, "utf-8");
364
+ const source = fs.readFileSync(filePath, "utf-8");
935
365
  return /:global\(:root\)\s*\{/.test(source);
936
366
  } catch {
937
367
  return false;
@@ -940,10 +370,10 @@ function themeFileApi(opts) {
940
370
  function listComponentSourcePaths() {
941
371
  const byName = /* @__PURE__ */ new Map();
942
372
  for (const dir of COMPONENTS_SCAN_DIRS) {
943
- if (!fs2.existsSync(dir)) continue;
944
- for (const f of fs2.readdirSync(dir)) {
373
+ if (!fs.existsSync(dir)) continue;
374
+ for (const f of fs.readdirSync(dir)) {
945
375
  if (!f.endsWith(".svelte")) continue;
946
- const full = path2.join(dir, f);
376
+ const full = path.join(dir, f);
947
377
  if (!isThemeAwareComponent(full)) continue;
948
378
  const name = componentNameFromFile(f);
949
379
  if (!byName.has(name)) byName.set(name, full);
@@ -957,18 +387,18 @@ function themeFileApi(opts) {
957
387
  function warnOnTokenDrift(log) {
958
388
  let tokensCss = "";
959
389
  try {
960
- tokensCss = fs2.readFileSync(CSS_PATH, "utf-8");
390
+ tokensCss = fs.readFileSync(CSS_PATH, "utf-8");
961
391
  } catch {
962
392
  return;
963
393
  }
964
394
  let generatedCss = "";
965
395
  try {
966
- generatedCss = fs2.readFileSync(GENERATED_CSS_PATH, "utf-8");
396
+ generatedCss = fs.readFileSync(GENERATED_CSS_PATH, "utf-8");
967
397
  } catch {
968
398
  }
969
399
  const componentSources = listComponentSourcePaths().map((p) => ({
970
400
  name: componentNameFromFile(p),
971
- source: fs2.readFileSync(p, "utf-8")
401
+ source: fs.readFileSync(p, "utf-8")
972
402
  }));
973
403
  const missing = validateTokensCss({ tokensCss, generatedCss, componentSources });
974
404
  if (missing.length === 0) return;
@@ -976,7 +406,7 @@ function themeFileApi(opts) {
976
406
  (m) => ` ${m.token} (referenced by ${m.referencedBy.join(", ")})`
977
407
  );
978
408
  log(
979
- `[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${path2.relative(process.cwd(), CSS_PATH)}:
409
+ `[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${path.relative(process.cwd(), CSS_PATH)}:
980
410
  ${lines.join("\n")}
981
411
  These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
982
412
  );
@@ -984,37 +414,37 @@ ${lines.join("\n")}
984
414
  function autoMigrateAdditive(log) {
985
415
  let before = "";
986
416
  try {
987
- before = fs2.readFileSync(CSS_PATH, "utf-8");
417
+ before = fs.readFileSync(CSS_PATH, "utf-8");
988
418
  } catch {
989
419
  return;
990
420
  }
991
421
  const { css, applied, changed } = runAdditiveTokensCssMigrations(before);
992
422
  if (!changed) return;
993
- fs2.writeFileSync(CSS_PATH, css);
423
+ fs.writeFileSync(CSS_PATH, css);
994
424
  log(
995
- `[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${path2.relative(process.cwd(), CSS_PATH)}: ${applied.join(", ")}. Review the diff in git.`
425
+ `[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${path.relative(process.cwd(), CSS_PATH)}: ${applied.join(", ")}. Review the diff in git.`
996
426
  );
997
427
  }
998
428
  function generateDefaultConfig(comp, sourcePath) {
999
- if (!fs2.existsSync(sourcePath)) return;
429
+ if (!fs.existsSync(sourcePath)) return;
1000
430
  const r = componentResource(comp);
1001
431
  r.ensureDir();
1002
432
  const defaultPath = r.filePath("default");
1003
- const sourceStat = fs2.statSync(sourcePath);
1004
- if (fs2.existsSync(defaultPath)) {
1005
- const defaultStat = fs2.statSync(defaultPath);
433
+ const sourceStat = fs.statSync(sourcePath);
434
+ if (fs.existsSync(defaultPath)) {
435
+ const defaultStat = fs.statSync(defaultPath);
1006
436
  if (defaultStat.mtimeMs >= sourceStat.mtimeMs) return;
1007
437
  }
1008
- const source = fs2.readFileSync(sourcePath, "utf-8");
438
+ const source = fs.readFileSync(sourcePath, "utf-8");
1009
439
  const body = extractGlobalRootBody(source);
1010
440
  const aliases = extractAliasDeclarations(body);
1011
441
  const now = (/* @__PURE__ */ new Date()).toISOString();
1012
442
  let createdAt = now;
1013
443
  let existingUpdatedAt;
1014
444
  let existingAliases;
1015
- if (fs2.existsSync(defaultPath)) {
445
+ if (fs.existsSync(defaultPath)) {
1016
446
  try {
1017
- const existing = JSON.parse(fs2.readFileSync(defaultPath, "utf-8"));
447
+ const existing = JSON.parse(fs.readFileSync(defaultPath, "utf-8"));
1018
448
  if (existing.createdAt) createdAt = existing.createdAt;
1019
449
  if (typeof existing.updatedAt === "string") existingUpdatedAt = existing.updatedAt;
1020
450
  if (existing.aliases && typeof existing.aliases === "object") {
@@ -1040,57 +470,191 @@ ${lines.join("\n")}
1040
470
  };
1041
471
  if (aliasesUnchanged && existingUpdatedAt) {
1042
472
  const t = /* @__PURE__ */ new Date();
1043
- fs2.utimesSync(defaultPath, t, t);
473
+ fs.utimesSync(defaultPath, t, t);
1044
474
  return;
1045
475
  }
1046
- fs2.writeFileSync(defaultPath, JSON.stringify(defaultConfig, null, 2));
476
+ fs.writeFileSync(defaultPath, JSON.stringify(defaultConfig, null, 2));
1047
477
  }
1048
478
  function ensureComponentConfigsDir() {
1049
- if (!fs2.existsSync(COMPONENT_CONFIGS_DIR)) {
1050
- fs2.mkdirSync(COMPONENT_CONFIGS_DIR, { recursive: true });
479
+ if (!fs.existsSync(COMPONENT_CONFIGS_DIR)) {
480
+ fs.mkdirSync(COMPONENT_CONFIGS_DIR, { recursive: true });
1051
481
  }
1052
482
  for (const sourcePath of listComponentSourcePaths()) {
1053
483
  const comp = componentNameFromFile(sourcePath);
1054
484
  const r = componentResource(comp);
1055
485
  r.ensureDir();
1056
486
  generateDefaultConfig(comp, sourcePath);
1057
- r.ensureMeta();
1058
487
  }
1059
488
  }
1060
- function ensureManifestsDir() {
1061
- manifestsResource.ensureDir();
1062
- if (!fs2.existsSync(manifestsResource.activePath)) {
1063
- fs2.writeFileSync(
1064
- manifestsResource.activePath,
1065
- JSON.stringify({ activeFile: "default" })
1066
- );
1067
- } else {
1068
- const activeName = manifestsResource.getActiveName();
1069
- if (manifestsResource.existingPath(activeName) === null) {
1070
- manifestsResource.setActiveName("default");
489
+ function listLegacyPointers() {
490
+ const legacy = [];
491
+ const check = (dir) => {
492
+ for (const name of ["_active.json", "_production.json"]) {
493
+ const p = path.join(dir, name);
494
+ if (fs.existsSync(p)) legacy.push(path.relative(process.cwd(), p));
495
+ }
496
+ };
497
+ check(COLORS_AND_TYPE_DIR);
498
+ if (fs.existsSync(COMPONENT_CONFIGS_DIR)) {
499
+ for (const entry of fs.readdirSync(COMPONENT_CONFIGS_DIR, { withFileTypes: true })) {
500
+ if (entry.isDirectory()) check(path.join(COMPONENT_CONFIGS_DIR, entry.name));
1071
501
  }
1072
502
  }
503
+ return legacy;
1073
504
  }
1074
- function patchActiveManifest(field, comp, fileName) {
1075
- const activeFile = manifestsResource.getActiveName();
1076
- if (activeFile === "default") return false;
1077
- const manifestPath = manifestsResource.filePath(activeFile);
1078
- if (!fs2.existsSync(manifestPath)) return false;
1079
- let manifest;
505
+ function warnOnLegacyPointers(legacy, bakeHeld, warn) {
506
+ if (legacy.length === 0) return;
507
+ warn(
508
+ `[live-tokens] ${legacy.length} legacy pointer file(s) left from the pre-working-set data model, starting with ${legacy[0]}. They are no longer read. Run \`npx live-tokens migrate\` to heal the tree.` + (bakeHeld ? ` Until then this tree has named no production theme, so ${path.basename(GENERATED_CSS_PATH)} is left exactly as it is rather than rebuilt from the default.` : "")
509
+ );
510
+ }
511
+ function warnOnLegacyLayout(evidence, warn) {
512
+ warn(
513
+ `[live-tokens] This project still uses the data layout from 0.47 and earlier: ${evidence}. Since 0.48 the whole-look theme files live in \`themes/\` and the colors and type live in \`colors-and-type/\`. Nothing in the data directory was written, the editor cannot save, and ${path.basename(GENERATED_CSS_PATH)} is left exactly as it is. Run \`npx live-tokens migrate\` to move the directories and heal the tree, then restart the dev server.`
514
+ );
515
+ }
516
+ const diskThemeResolvers = {
517
+ readColorsAndType: (name) => colorsAndTypeResource.readJson(sanitizeFileName(name)),
518
+ readComponentConfig: (comp, name) => readComponentConfig(comp, sanitizeFileName(name)),
519
+ normalizeColorsAndType: (colorsAndType) => normalizeColorsAndType(colorsAndType)
520
+ };
521
+ function readTheme(fileName) {
522
+ let raw;
1080
523
  try {
1081
- manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
524
+ raw = themesResource.readJson(fileName);
1082
525
  } catch {
1083
- return false;
526
+ return null;
1084
527
  }
1085
- if (field === "theme") {
1086
- manifest.theme = fileName;
1087
- } else if (field === "component" && comp) {
1088
- manifest.componentConfigs = manifest.componentConfigs ?? {};
1089
- manifest.componentConfigs[comp] = fileName;
528
+ if (!raw) return null;
529
+ if (isColorsAndTypeShaped(raw)) return null;
530
+ return normalizeTheme(raw, diskThemeResolvers);
531
+ }
532
+ function unreadableThemeReason(fileName) {
533
+ const resolved = themesResource.existingPath(fileName);
534
+ if (resolved === null) return "missing";
535
+ try {
536
+ if (isColorsAndTypeShaped(JSON.parse(fs.readFileSync(resolved, "utf-8")))) {
537
+ return "colors-and-type";
538
+ }
539
+ } catch {
1090
540
  }
1091
- manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1092
- fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1093
- return true;
541
+ return "corrupt";
542
+ }
543
+ function notAThemeError(fileName) {
544
+ return `"${fileName}" is a colors-and-type file, not a theme. Before 0.48 those lived in themes/; move it to ${path.basename(COLORS_AND_TYPE_DIR)}/ or delete it.`;
545
+ }
546
+ function respondUnreadableTheme(res, fileName, missingError) {
547
+ switch (unreadableThemeReason(fileName)) {
548
+ case "missing":
549
+ jsonResponse(res, 404, { error: missingError });
550
+ return;
551
+ case "colors-and-type":
552
+ jsonResponse(res, 422, { error: notAThemeError(fileName), code: "CORRUPT_THEME" });
553
+ return;
554
+ case "corrupt":
555
+ jsonResponse(res, 422, {
556
+ error: `Theme file "${fileName}" is not valid JSON`,
557
+ code: "CORRUPT_THEME"
558
+ });
559
+ }
560
+ }
561
+ function writeTheme(fileName, theme) {
562
+ themesResource.ensureDir();
563
+ fs.writeFileSync(themesResource.filePath(fileName), JSON.stringify(theme, null, 2));
564
+ }
565
+ function migrateLocalThemes(warn) {
566
+ if (path.resolve(THEMES_DIR) === path.resolve(packageThemesDir)) return;
567
+ if (!fs.existsSync(THEMES_DIR)) return;
568
+ for (const file of fs.readdirSync(THEMES_DIR)) {
569
+ if (!file.endsWith(".json") || file.startsWith("_")) continue;
570
+ const fileName = file.slice(0, -".json".length);
571
+ if (fileName === "default") continue;
572
+ let raw;
573
+ try {
574
+ raw = JSON.parse(fs.readFileSync(path.join(THEMES_DIR, file), "utf-8"));
575
+ } catch {
576
+ continue;
577
+ }
578
+ if (isColorsAndTypeShaped(raw)) {
579
+ warn(`[live-tokens] ${notAThemeError(fileName)} It is left untouched.`);
580
+ continue;
581
+ }
582
+ const { theme, dropped, migrated } = normalizeTheme(raw, diskThemeResolvers);
583
+ if (!migrated) continue;
584
+ writeTheme(fileName, theme);
585
+ if (dropped.length > 0) {
586
+ warn(
587
+ `[live-tokens] Theme "${fileName}": ${dropped.join(", ")} no longer exists; those slices now use their defaults.`
588
+ );
589
+ }
590
+ }
591
+ }
592
+ function ensureDefaultTheme(warn) {
593
+ let existing = null;
594
+ try {
595
+ existing = JSON.parse(fs.readFileSync(themesResource.filePath("default"), "utf-8"));
596
+ } catch {
597
+ }
598
+ if (isColorsAndTypeShaped(existing)) {
599
+ warn(`[live-tokens] ${notAThemeError("default")} The Default theme was not materialised.`);
600
+ return;
601
+ }
602
+ const colorsAndType = normalizeColorsAndType(colorsAndTypeResource.readJson("default"));
603
+ const componentConfigs = {};
604
+ for (const comp of listComponentNames()) {
605
+ const cfg = readComponentConfig(comp, "default");
606
+ if (cfg) componentConfigs[comp] = cfg;
607
+ }
608
+ if (existing?.schemaVersion === THEME_SCHEMA_VERSION && JSON.stringify(existing.colorsAndType) === JSON.stringify(colorsAndType) && JSON.stringify(existing.componentConfigs) === JSON.stringify(componentConfigs)) {
609
+ return;
610
+ }
611
+ const now = (/* @__PURE__ */ new Date()).toISOString();
612
+ writeTheme("default", {
613
+ name: "Motion Proto",
614
+ createdAt: typeof existing?.createdAt === "string" ? existing.createdAt : now,
615
+ updatedAt: now,
616
+ schemaVersion: THEME_SCHEMA_VERSION,
617
+ colorsAndType,
618
+ componentConfigs
619
+ });
620
+ }
621
+ function ensureThemesDir(warn) {
622
+ themesResource.ensureDir();
623
+ ensureDefaultTheme(warn);
624
+ migrateLocalThemes(warn);
625
+ const productionPointerHeld = !fs.existsSync(themesResource.productionPath) && listLegacyPointers().length > 0;
626
+ if (productionPointerHeld) {
627
+ if (!fs.existsSync(themesResource.activePath)) themesResource.setActiveName("default");
628
+ } else {
629
+ themesResource.ensureMeta();
630
+ }
631
+ if (themesResource.existingPath(themesResource.getActiveName()) === null) {
632
+ themesResource.setActiveName("default");
633
+ }
634
+ if (themesResource.existingPath(themesResource.getProductionName()) === null) {
635
+ themesResource.setProductionName("default");
636
+ }
637
+ return { productionPointerHeld };
638
+ }
639
+ function activeTheme() {
640
+ return readTheme(themesResource.getActiveName())?.theme ?? null;
641
+ }
642
+ function resolveLiveColorsAndType(theme) {
643
+ const working = colorsAndTypeResource.readWorking();
644
+ if (working !== null) return { data: normalizeColorsAndType(working), source: "working" };
645
+ if (theme?.colorsAndType) return { data: theme.colorsAndType, source: "theme" };
646
+ const shipped = colorsAndTypeResource.readJson("default");
647
+ if (!shipped) return null;
648
+ return { data: normalizeColorsAndType(shipped), source: "default" };
649
+ }
650
+ function resolveLiveComponentConfig(comp, theme) {
651
+ const working = componentResource(comp).readWorking();
652
+ if (working !== null) return { data: working, source: "working" };
653
+ const embedded = theme?.componentConfigs[comp];
654
+ if (embedded) return { data: { ...embedded, component: comp }, source: "theme" };
655
+ const cfg = readComponentConfig(comp, "default");
656
+ if (!cfg) return null;
657
+ return { data: cfg, source: "default" };
1094
658
  }
1095
659
  function formatAliasGradient(v) {
1096
660
  const stopColor = (s) => {
@@ -1126,237 +690,191 @@ ${lines.join("\n")}
1126
690
  return null;
1127
691
  }
1128
692
  }
1129
- function syncComponentsToCss() {
1130
- regenerateTokensCss();
1131
- }
1132
693
  const escapedBase = API_BASE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
694
+ const COLORS_AND_TYPE_ROUTE = `${API_BASE}/colors-and-type`;
695
+ const COLORS_AND_TYPE_ACTIVE_ROUTE = `${API_BASE}/colors-and-type/active`;
696
+ const COLORS_AND_TYPE_PRODUCTION_ROUTE = `${API_BASE}/colors-and-type/production`;
697
+ const COLORS_AND_TYPE_WORKING_ROUTE = `${API_BASE}/colors-and-type/working`;
698
+ const COMPONENT_CONFIGS_ROUTE = `${API_BASE}/component-configs`;
1133
699
  const THEMES_ROUTE = `${API_BASE}/themes`;
1134
700
  const THEMES_ACTIVE_ROUTE = `${API_BASE}/themes/active`;
1135
701
  const THEMES_PRODUCTION_ROUTE = `${API_BASE}/themes/production`;
1136
- const COMPONENT_CONFIGS_ROUTE = `${API_BASE}/component-configs`;
1137
- const MANIFESTS_ROUTE = `${API_BASE}/manifests`;
1138
- const MANIFESTS_ACTIVE_ROUTE = `${API_BASE}/manifests/active`;
1139
- const THEME_BY_NAME_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)$`);
702
+ const COLORS_AND_TYPE_BY_NAME_REGEX = new RegExp(`^${escapedBase}/colors-and-type/([a-z0-9\\-_]+)$`);
1140
703
  const COMP_LIST_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)$`);
1141
704
  const COMP_ACTIVE_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/active$`);
1142
705
  const COMP_PRODUCTION_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/production$`);
706
+ const COMP_WORKING_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/working$`);
1143
707
  const COMP_BY_NAME_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/([a-z0-9\\-_]+)$`);
1144
- const MANIFEST_APPLY_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/apply$`);
1145
- const MANIFEST_EXPORT_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/export$`);
1146
- const MANIFEST_IMPORT_ROUTE = `${API_BASE}/manifests/import`;
1147
- const MANIFEST_BY_NAME_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)$`);
1148
- async function handleListThemes(_ctx) {
1149
- const activeFile = themesResource.getActiveName();
1150
- const files = themesResource.listNames().map((fileName) => {
1151
- const data = themesResource.readJson(fileName);
708
+ const THEME_APPLY_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)/apply$`);
709
+ const THEME_EXPORT_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)/export$`);
710
+ const THEME_IMPORT_ROUTE = `${API_BASE}/themes/import`;
711
+ const THEME_BY_NAME_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)$`);
712
+ const PRODUCTION_ROUTE = `${API_BASE}/production`;
713
+ async function handleListColorsAndType(_ctx) {
714
+ const files = colorsAndTypeResource.listNames().map((fileName) => {
715
+ const data = colorsAndTypeResource.readJson(fileName);
1152
716
  return {
1153
717
  name: data?.name || fileName,
1154
718
  fileName,
1155
719
  updatedAt: data?.updatedAt || "",
1156
- isActive: fileName === activeFile
720
+ // Package-owned rows are the shipped presets and the default. The Load
721
+ // window offers those as whole themes, so it lists only the rows a
722
+ // local file backs — including a local copy of a shipped name, which
723
+ // holds whatever the user did to it.
724
+ isPackage: colorsAndTypeResource.isPackageFile(fileName)
1157
725
  };
1158
726
  });
1159
727
  jsonResponse(_ctx.res, 200, { files });
1160
728
  }
1161
- async function handleGetActiveTheme({ res }) {
1162
- const activeFile = themesResource.getActiveName();
1163
- const raw = themesResource.readJson(activeFile);
1164
- if (!raw) {
1165
- jsonResponse(res, 404, { error: "Active theme not found" });
1166
- return;
1167
- }
1168
- const data = normalizeTheme(raw);
1169
- data._fileName = activeFile;
1170
- jsonResponse(res, 200, data);
1171
- }
1172
- async function handleSetActiveTheme({ req, res }) {
1173
- const body = JSON.parse(await readBody(req));
1174
- const fileName = sanitizeFileName(body.name || "default");
1175
- if (themesResource.existingPath(fileName) === null) {
1176
- jsonResponse(res, 404, { error: "Theme not found" });
1177
- return;
1178
- }
1179
- themesResource.setActiveName(fileName);
1180
- jsonResponse(res, 200, { ok: true, activeFile: fileName });
1181
- }
1182
- async function handleGetProductionTheme({ res }) {
1183
- const prodFile = themesResource.getProductionName();
1184
- const raw = themesResource.readJson(prodFile);
1185
- if (!raw) {
1186
- jsonResponse(res, 200, { fileName: prodFile, name: prodFile, cssVariables: {} });
729
+ async function handleGetActiveColorsAndType({ res }) {
730
+ const resolved = resolveLiveColorsAndType(activeTheme());
731
+ if (!resolved) {
732
+ jsonResponse(res, 404, { error: "Active colors and type not found" });
1187
733
  return;
1188
734
  }
1189
- const data = normalizeTheme(raw);
1190
735
  jsonResponse(res, 200, {
1191
- fileName: prodFile,
1192
- name: data.name || prodFile,
1193
- updatedAt: data.updatedAt || "",
1194
- cssVariables: data.cssVariables || {}
736
+ ...resolved.data,
737
+ _fileName: themesResource.getActiveName(),
738
+ _source: resolved.source
1195
739
  });
1196
740
  }
1197
- async function handleSetProductionTheme({ req, res }) {
1198
- const body = JSON.parse(await readBody(req));
1199
- const fileName = sanitizeFileName(body.name || "default");
1200
- if (themesResource.existingPath(fileName) === null) {
1201
- jsonResponse(res, 404, { error: "Theme not found" });
1202
- return;
741
+ async function readBufferBody(req, res) {
742
+ let parsed;
743
+ try {
744
+ parsed = JSON.parse(await readBody(req));
745
+ } catch {
746
+ parsed = void 0;
1203
747
  }
1204
- if (manifestsResource.getActiveName() === "default") {
1205
- jsonResponse(res, 409, {
1206
- error: "Active manifest is protected. Save As first.",
1207
- code: "ACTIVE_IS_PROTECTED"
1208
- });
748
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
749
+ jsonResponse(res, 400, { error: "Body must be a JSON object", code: "INVALID_BODY" });
750
+ return null;
751
+ }
752
+ return parsed;
753
+ }
754
+ async function handleGetWorkingColorsAndType({ res }) {
755
+ const raw = colorsAndTypeResource.readWorking();
756
+ if (raw === null) {
757
+ jsonResponse(res, 404, { error: "No working colors and type" });
1209
758
  return;
1210
759
  }
1211
- themesResource.setProductionName(fileName);
1212
- syncTokensToCss(fileName);
1213
- syncFontsToCss(fileName);
1214
- syncComponentsToCss();
1215
- patchActiveManifest("theme", null, fileName);
1216
- const data = themesResource.readJson(fileName);
1217
- jsonResponse(res, 200, {
1218
- ok: true,
1219
- fileName,
1220
- name: data?.name || fileName,
1221
- updatedAt: data?.updatedAt || ""
1222
- });
760
+ jsonResponse(res, 200, normalizeColorsAndType(raw));
1223
761
  }
1224
- async function handleThemeByName({ params, req, res }) {
762
+ async function handleSetWorkingColorsAndType({ req, res }) {
763
+ const body = await readBufferBody(req, res);
764
+ if (!body) return;
765
+ colorsAndTypeResource.writeWorking(body);
766
+ jsonResponse(res, 200, { ok: true });
767
+ }
768
+ async function handleClearWorkingColorsAndType({ res }) {
769
+ colorsAndTypeResource.clearWorking();
770
+ jsonResponse(res, 200, { ok: true });
771
+ }
772
+ async function handleColorsAndTypeByName({ params, req, res }) {
1225
773
  const [fileName] = params;
1226
- const filePath = themesResource.filePath(fileName);
774
+ if (rejectReservedName(res, fileName)) return;
775
+ const filePath = colorsAndTypeResource.filePath(fileName);
1227
776
  if (req.method === "GET") {
1228
- const raw = themesResource.readJson(fileName);
777
+ const raw = colorsAndTypeResource.readJson(fileName);
1229
778
  if (!raw) {
1230
779
  jsonResponse(res, 404, { error: "Not found" });
1231
780
  return;
1232
781
  }
1233
- const data = normalizeTheme(raw);
782
+ const data = normalizeColorsAndType(raw);
1234
783
  data._fileName = fileName;
1235
784
  jsonResponse(res, 200, data);
1236
785
  return;
1237
786
  }
1238
787
  if (req.method === "PUT") {
1239
788
  if (fileName === "default") {
1240
- jsonResponse(res, 403, { error: "Cannot overwrite the default theme (live from package)" });
789
+ jsonResponse(res, 403, { error: "Cannot overwrite the default colors and type (live from package)" });
1241
790
  return;
1242
791
  }
1243
792
  const body = JSON.parse(await readBody(req));
1244
793
  body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1245
- if (fs2.existsSync(filePath)) {
794
+ if (fs.existsSync(filePath)) {
1246
795
  try {
1247
- const existing = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
796
+ const existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1248
797
  if (existing.createdAt) body.createdAt = existing.createdAt;
1249
798
  } catch {
1250
799
  }
1251
800
  }
1252
801
  if (!body.createdAt) body.createdAt = body.updatedAt;
1253
- fs2.writeFileSync(filePath, JSON.stringify(body, null, 2));
1254
- if (fileName === themesResource.getProductionName()) {
1255
- syncTokensToCss(fileName);
1256
- syncFontsToCss(fileName);
1257
- syncComponentsToCss();
1258
- }
802
+ fs.writeFileSync(filePath, JSON.stringify(body, null, 2));
1259
803
  jsonResponse(res, 200, { ok: true, fileName });
1260
804
  return;
1261
805
  }
1262
806
  if (req.method === "DELETE") {
1263
807
  if (fileName === "default") {
1264
- jsonResponse(res, 403, { error: "Cannot delete the default theme" });
808
+ jsonResponse(res, 403, { error: "Cannot delete the default colors and type" });
1265
809
  return;
1266
810
  }
1267
- if (themesResource.getProductionName() === fileName) {
811
+ if (fs.existsSync(filePath)) {
812
+ fs.unlinkSync(filePath);
813
+ } else if (colorsAndTypeResource.existingPath(fileName)) {
1268
814
  jsonResponse(res, 403, {
1269
- error: "Cannot delete the production theme. Adopt a different theme first.",
1270
- code: "PRODUCTION_THEME"
815
+ error: "Cannot delete colors and type shipped with the package. Saving it creates a local copy; deleting that copy restores the shipped version.",
816
+ code: "PACKAGE_COLORS_AND_TYPE"
1271
817
  });
1272
818
  return;
1273
819
  }
1274
- if (fs2.existsSync(filePath)) {
1275
- fs2.unlinkSync(filePath);
1276
- if (themesResource.getActiveName() === fileName) {
1277
- themesResource.setActiveName("default");
1278
- }
1279
- }
1280
820
  jsonResponse(res, 200, { ok: true });
1281
821
  return;
1282
822
  }
1283
823
  }
1284
824
  async function handleListComponents({ res }) {
1285
- const components = listComponentNames().map((comp) => {
1286
- const r = componentResource(comp);
1287
- return {
1288
- name: comp,
1289
- activeFile: r.getActiveName(),
1290
- productionFile: r.getProductionName()
1291
- };
1292
- });
825
+ const theme = activeTheme();
826
+ const components = listComponentNames().map((comp) => ({
827
+ name: comp,
828
+ // Presence, not a parse: one corrupt buffer must not take the listing
829
+ // down, and the door only has to say where each component's live config
830
+ // comes from.
831
+ source: componentResource(comp).hasWorking() ? "working" : theme?.componentConfigs[comp] ? "theme" : "default"
832
+ }));
1293
833
  jsonResponse(res, 200, { components });
1294
834
  }
835
+ function rejectUnknownComponent(res, comp) {
836
+ if (listComponentNames().includes(comp)) return false;
837
+ jsonResponse(res, 404, { error: `Unknown component "${comp}"`, code: "UNKNOWN_COMPONENT" });
838
+ return true;
839
+ }
1295
840
  async function handleGetComponentActive({ params, res }) {
1296
841
  const [comp] = params;
1297
- const r = componentResource(comp);
1298
- const activeFile = r.getActiveName();
1299
- const cfg = readComponentConfig(comp, activeFile);
1300
- if (!cfg) {
842
+ const resolved = resolveLiveComponentConfig(comp, activeTheme());
843
+ if (!resolved) {
1301
844
  jsonResponse(res, 404, { error: "Active config not found" });
1302
845
  return;
1303
846
  }
1304
- jsonResponse(res, 200, { ...cfg, _fileName: activeFile });
847
+ jsonResponse(res, 200, {
848
+ ...resolved.data,
849
+ _fileName: themesResource.getActiveName(),
850
+ _source: resolved.source
851
+ });
1305
852
  }
1306
- async function handleSetComponentActive({ params, req, res }) {
853
+ async function handleGetComponentWorking({ params, res }) {
1307
854
  const [comp] = params;
1308
- const body = JSON.parse(await readBody(req));
1309
- const fileName = sanitizeFileName(body.name || "default");
1310
- const r = componentResource(comp);
1311
- if (r.existingPath(fileName) === null) {
1312
- jsonResponse(res, 404, { error: "Config not found" });
855
+ const raw = componentResource(comp).readWorking();
856
+ if (raw === null) {
857
+ jsonResponse(res, 404, { error: "No working config" });
1313
858
  return;
1314
859
  }
1315
- r.ensureDir();
1316
- r.setActiveName(fileName);
1317
- jsonResponse(res, 200, { ok: true, activeFile: fileName });
860
+ jsonResponse(res, 200, raw);
1318
861
  }
1319
- async function handleGetComponentProduction({ params, res }) {
862
+ async function handleSetComponentWorking({ params, req, res }) {
1320
863
  const [comp] = params;
1321
- const r = componentResource(comp);
1322
- const prodFile = r.getProductionName();
1323
- const cfg = readComponentConfig(comp, prodFile);
1324
- jsonResponse(res, 200, {
1325
- fileName: prodFile,
1326
- name: cfg?.name || prodFile,
1327
- aliases: cfg?.aliases || {}
1328
- });
864
+ if (rejectUnknownComponent(res, comp)) return;
865
+ const body = await readBufferBody(req, res);
866
+ if (!body) return;
867
+ componentResource(comp).writeWorking(body);
868
+ jsonResponse(res, 200, { ok: true });
1329
869
  }
1330
- async function handleSetComponentProduction({ params, req, res }) {
870
+ async function handleClearComponentWorking({ params, res }) {
1331
871
  const [comp] = params;
1332
- const body = JSON.parse(await readBody(req));
1333
- const fileName = sanitizeFileName(body.name || "default");
1334
- const r = componentResource(comp);
1335
- if (r.existingPath(fileName) === null) {
1336
- jsonResponse(res, 404, { error: "Config not found" });
1337
- return;
1338
- }
1339
- if (manifestsResource.getActiveName() === "default") {
1340
- jsonResponse(res, 409, {
1341
- error: "Active manifest is protected. Save As first.",
1342
- code: "ACTIVE_IS_PROTECTED"
1343
- });
1344
- return;
1345
- }
1346
- r.ensureDir();
1347
- r.setProductionName(fileName);
1348
- syncComponentsToCss();
1349
- patchActiveManifest("component", comp, fileName);
1350
- const cfg = readComponentConfig(comp, fileName);
1351
- jsonResponse(res, 200, {
1352
- ok: true,
1353
- fileName,
1354
- name: cfg?.name || fileName,
1355
- aliases: cfg?.aliases || {}
1356
- });
872
+ componentResource(comp).clearWorking();
873
+ jsonResponse(res, 200, { ok: true });
1357
874
  }
1358
875
  async function handleComponentConfigByName({ params, req, res }) {
1359
876
  const [comp, name] = params;
877
+ if (rejectReservedName(res, name)) return;
1360
878
  const r = componentResource(comp);
1361
879
  const configPath = r.filePath(name);
1362
880
  if (req.method === "GET") {
@@ -1379,19 +897,16 @@ ${lines.join("\n")}
1379
897
  body.component = comp;
1380
898
  body.name = body.name || name;
1381
899
  body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1382
- if (fs2.existsSync(configPath)) {
900
+ if (fs.existsSync(configPath)) {
1383
901
  try {
1384
- const existing = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
902
+ const existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
1385
903
  if (existing.createdAt) body.createdAt = existing.createdAt;
1386
904
  } catch {
1387
905
  }
1388
906
  }
1389
907
  if (!body.createdAt) body.createdAt = body.updatedAt;
1390
908
  r.ensureDir();
1391
- fs2.writeFileSync(configPath, JSON.stringify(body, null, 2));
1392
- if (r.getProductionName() === name) {
1393
- syncComponentsToCss();
1394
- }
909
+ fs.writeFileSync(configPath, JSON.stringify(body, null, 2));
1395
910
  jsonResponse(res, 200, { ok: true, fileName: name });
1396
911
  return;
1397
912
  }
@@ -1400,16 +915,7 @@ ${lines.join("\n")}
1400
915
  jsonResponse(res, 403, { error: "Cannot delete default config" });
1401
916
  return;
1402
917
  }
1403
- if (fs2.existsSync(configPath)) {
1404
- fs2.unlinkSync(configPath);
1405
- if (r.getActiveName() === name) {
1406
- r.setActiveName("default");
1407
- }
1408
- if (r.getProductionName() === name) {
1409
- r.setProductionName("default");
1410
- syncComponentsToCss();
1411
- }
1412
- }
918
+ if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
1413
919
  jsonResponse(res, 200, { ok: true });
1414
920
  return;
1415
921
  }
@@ -1417,199 +923,190 @@ ${lines.join("\n")}
1417
923
  async function handleListComponentConfigs({ params, res }) {
1418
924
  const [comp] = params;
1419
925
  const r = componentResource(comp);
1420
- if (!fs2.existsSync(r.dir)) {
926
+ if (!fs.existsSync(r.dir)) {
1421
927
  jsonResponse(res, 404, { error: "Component not found" });
1422
928
  return;
1423
929
  }
1424
- const activeFile = r.getActiveName();
1425
- const productionFile = r.getProductionName();
1426
930
  const files = r.listNames().map((fileName) => {
1427
931
  const data = r.readJson(fileName);
1428
932
  return {
1429
933
  name: data?.name || fileName,
1430
934
  fileName,
1431
- updatedAt: data?.updatedAt || "",
1432
- isActive: fileName === activeFile,
1433
- isProduction: fileName === productionFile
935
+ updatedAt: data?.updatedAt || ""
1434
936
  };
1435
937
  });
1436
- jsonResponse(res, 200, { component: comp, files, activeFile, productionFile });
938
+ jsonResponse(res, 200, { component: comp, files });
1437
939
  }
1438
- async function handleListManifests({ res }) {
1439
- const activeFile = manifestsResource.getActiveName();
1440
- const files = manifestsResource.listNames().map((fileName) => {
1441
- const data = manifestsResource.readJson(fileName);
1442
- return {
1443
- name: data?.name || fileName,
940
+ async function handleListThemes({ res }) {
941
+ const activeFile = themesResource.getActiveName();
942
+ const productionFile = themesResource.getProductionName();
943
+ const files = [];
944
+ for (const fileName of themesResource.listNames()) {
945
+ const read = readTheme(fileName);
946
+ if (!read) {
947
+ console.warn(
948
+ unreadableThemeReason(fileName) === "colors-and-type" ? `[live-tokens] ${notAThemeError(fileName)} It is left out of the theme list.` : `[live-tokens] Theme "${fileName}" is unreadable and was left out of the list.`
949
+ );
950
+ continue;
951
+ }
952
+ files.push({
953
+ name: read.theme.name,
1444
954
  fileName,
1445
- updatedAt: data?.updatedAt || "",
955
+ updatedAt: read.theme.updatedAt,
1446
956
  isActive: fileName === activeFile,
957
+ isProduction: fileName === productionFile,
1447
958
  isProtected: fileName === "default"
1448
- };
1449
- });
1450
- jsonResponse(res, 200, { files, activeFile });
959
+ });
960
+ }
961
+ jsonResponse(res, 200, { files, activeFile, productionFile });
1451
962
  }
1452
- async function handleGetActiveManifest({ res }) {
1453
- const activeFile = manifestsResource.getActiveName();
1454
- const raw = manifestsResource.readJson(activeFile);
1455
- if (!raw) {
1456
- jsonResponse(res, 404, { error: "Active manifest not found" });
963
+ async function handleGetProductionTheme({ res }) {
964
+ const fileName = themesResource.getProductionName();
965
+ const read = readTheme(fileName);
966
+ if (!read) {
967
+ respondUnreadableTheme(res, fileName, "Production theme not found");
968
+ return;
969
+ }
970
+ jsonResponse(res, 200, { ...read.theme, _fileName: fileName });
971
+ }
972
+ async function handleGetActiveTheme({ res }) {
973
+ const activeFile = themesResource.getActiveName();
974
+ const read = readTheme(activeFile);
975
+ if (!read) {
976
+ respondUnreadableTheme(res, activeFile, "Active theme not found");
1457
977
  return;
1458
978
  }
1459
- const data = raw;
1460
- data._fileName = activeFile;
1461
- jsonResponse(res, 200, data);
979
+ jsonResponse(res, 200, { ...read.theme, _fileName: activeFile });
1462
980
  }
1463
- async function handleSetActiveManifest({ req, res }) {
981
+ async function handleSetActiveTheme({ req, res }) {
1464
982
  const body = JSON.parse(await readBody(req));
1465
983
  const fileName = sanitizeFileName(body.name || "default");
1466
- if (manifestsResource.existingPath(fileName) === null) {
1467
- jsonResponse(res, 404, { error: "Manifest not found" });
984
+ if (themesResource.existingPath(fileName) === null) {
985
+ jsonResponse(res, 404, { error: "Theme not found" });
1468
986
  return;
1469
987
  }
1470
- manifestsResource.setActiveName(fileName);
988
+ themesResource.setActiveName(fileName);
1471
989
  jsonResponse(res, 200, { ok: true, activeFile: fileName });
1472
990
  }
1473
- async function handleManifestByName({ params, req, res }) {
991
+ async function handleThemeByName({ params, req, res }) {
1474
992
  const [fileName] = params;
1475
- const filePath = manifestsResource.filePath(fileName);
993
+ if (rejectReservedName(res, fileName)) return;
994
+ const filePath = themesResource.filePath(fileName);
1476
995
  if (req.method === "GET") {
1477
- const raw = manifestsResource.readJson(fileName);
1478
- if (!raw) {
1479
- jsonResponse(res, 404, { error: "Not found" });
996
+ const read = readTheme(fileName);
997
+ if (!read) {
998
+ respondUnreadableTheme(res, fileName, "Not found");
1480
999
  return;
1481
1000
  }
1482
- const data = raw;
1483
- data._fileName = fileName;
1484
- jsonResponse(res, 200, data);
1001
+ jsonResponse(res, 200, { ...read.theme, _fileName: fileName });
1485
1002
  return;
1486
1003
  }
1487
1004
  if (req.method === "PUT") {
1488
1005
  if (fileName === "default") {
1489
- jsonResponse(res, 403, { error: "Cannot overwrite the default manifest" });
1006
+ jsonResponse(res, 403, { error: "Cannot overwrite the default theme" });
1490
1007
  return;
1491
1008
  }
1492
1009
  const body = JSON.parse(await readBody(req));
1493
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1494
- if (fs2.existsSync(filePath)) {
1010
+ const { theme } = normalizeTheme(body, diskThemeResolvers);
1011
+ theme.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1012
+ if (fs.existsSync(filePath)) {
1495
1013
  try {
1496
- const existing = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1497
- if (existing.createdAt) body.createdAt = existing.createdAt;
1014
+ const existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1015
+ if (existing.createdAt) theme.createdAt = existing.createdAt;
1498
1016
  } catch {
1499
1017
  }
1500
1018
  }
1501
- if (!body.createdAt) body.createdAt = body.updatedAt;
1502
- fs2.writeFileSync(filePath, JSON.stringify(body, null, 2));
1019
+ writeTheme(fileName, theme);
1503
1020
  jsonResponse(res, 200, { ok: true, fileName });
1504
1021
  return;
1505
1022
  }
1506
1023
  if (req.method === "DELETE") {
1507
1024
  if (fileName === "default") {
1508
- jsonResponse(res, 403, { error: "Cannot delete the default manifest" });
1025
+ jsonResponse(res, 403, { error: "Cannot delete the default theme" });
1026
+ return;
1027
+ }
1028
+ if (fileName === themesResource.getProductionName()) {
1029
+ jsonResponse(res, 403, {
1030
+ error: "Cannot delete the theme that is in production. Adopt another theme first.",
1031
+ code: "PRODUCTION_THEME"
1032
+ });
1509
1033
  return;
1510
1034
  }
1511
- if (fs2.existsSync(filePath)) {
1512
- fs2.unlinkSync(filePath);
1513
- if (manifestsResource.getActiveName() === fileName) {
1514
- manifestsResource.setActiveName("default");
1035
+ if (fs.existsSync(filePath)) {
1036
+ fs.unlinkSync(filePath);
1037
+ if (themesResource.existingPath(fileName) === null && themesResource.getActiveName() === fileName) {
1038
+ themesResource.setActiveName("default");
1515
1039
  }
1040
+ } else if (themesResource.existingPath(fileName)) {
1041
+ jsonResponse(res, 403, {
1042
+ error: "Cannot delete a theme shipped with the package. Saving it creates a local copy; deleting that copy restores the shipped version.",
1043
+ code: "PACKAGE_THEME"
1044
+ });
1045
+ return;
1516
1046
  }
1517
1047
  jsonResponse(res, 200, { ok: true });
1518
1048
  return;
1519
1049
  }
1520
1050
  }
1521
- async function handleApplyManifest({ params, res }) {
1051
+ async function handleApplyTheme({ params, res }) {
1522
1052
  const [fileName] = params;
1523
- const manifest = manifestsResource.readJson(fileName);
1524
- if (!manifest) {
1525
- jsonResponse(res, 404, { error: "Manifest not found" });
1053
+ if (rejectReservedName(res, fileName)) return;
1054
+ const read = readTheme(fileName);
1055
+ if (!read) {
1056
+ respondUnreadableTheme(res, fileName, "Theme not found");
1526
1057
  return;
1527
1058
  }
1528
- const themeName = sanitizeFileName(manifest.theme || "default");
1529
- if (themesResource.existingPath(themeName) === null) {
1530
- jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
1059
+ const { theme } = read;
1060
+ const isDefault = fileName === "default";
1061
+ if (!isDefault && !theme.colorsAndType) {
1062
+ jsonResponse(res, 422, { error: "This theme carries no colors and type" });
1531
1063
  return;
1532
1064
  }
1533
- const knownComponents = new Set(listComponentNames());
1534
- const componentConfigs = manifest.componentConfigs ?? {};
1535
- const resolvedConfigs = {};
1536
- const apply = [];
1537
- for (const [comp, configFile] of Object.entries(componentConfigs)) {
1538
- if (!knownComponents.has(comp)) continue;
1539
- const sanitized = sanitizeFileName(String(configFile) || "default");
1540
- const r = componentResource(comp);
1541
- if (r.existingPath(sanitized) === null) {
1542
- jsonResponse(res, 422, {
1543
- error: `Manifest references missing config: ${comp}/${sanitized}`
1544
- });
1545
- return;
1546
- }
1547
- apply.push([comp, sanitized]);
1548
- }
1549
- themesResource.setActiveName(themeName);
1550
- themesResource.setProductionName(themeName);
1551
- syncTokensToCss(themeName);
1552
- syncFontsToCss(themeName);
1553
- const themeData = normalizeTheme(themesResource.readJson(themeName));
1554
- themeData._fileName = themeName;
1555
- for (const [comp, configFile] of apply) {
1065
+ if (isDefault) colorsAndTypeResource.clearWorking();
1066
+ else colorsAndTypeResource.writeWorking(theme.colorsAndType);
1067
+ const knownComponents = listComponentNames();
1068
+ for (const comp of knownComponents) {
1069
+ const embedded = isDefault ? void 0 : theme.componentConfigs[comp];
1556
1070
  const r = componentResource(comp);
1557
- r.setActiveName(configFile);
1558
- r.setProductionName(configFile);
1559
- const cfg = readComponentConfig(comp, configFile);
1560
- if (cfg) resolvedConfigs[comp] = { ...cfg, _fileName: configFile };
1071
+ if (embedded) r.writeWorking({ ...embedded, component: comp });
1072
+ else r.clearWorking();
1561
1073
  }
1074
+ themesResource.setActiveName(fileName);
1075
+ const resolvedConfigs = {};
1562
1076
  for (const comp of knownComponents) {
1563
- if (resolvedConfigs[comp]) continue;
1564
- const activeName = componentResource(comp).getActiveName();
1565
- const cfg = readComponentConfig(comp, activeName);
1566
- if (cfg) resolvedConfigs[comp] = { ...cfg, _fileName: activeName };
1077
+ const resolved = resolveLiveComponentConfig(comp, theme);
1078
+ if (resolved) resolvedConfigs[comp] = { ...resolved.data, _fileName: fileName, _source: resolved.source };
1567
1079
  }
1568
- syncComponentsToCss();
1569
- manifestsResource.setActiveName(fileName);
1080
+ const resolvedColorsAndType = resolveLiveColorsAndType(theme);
1081
+ const skippedComponents = Object.keys(theme.componentConfigs).filter(
1082
+ (comp) => !knownComponents.includes(comp)
1083
+ );
1570
1084
  jsonResponse(res, 200, {
1571
1085
  ok: true,
1572
- manifest: { ...manifest, _fileName: fileName },
1573
- theme: themeData,
1574
- componentConfigs: resolvedConfigs
1086
+ theme: { ...theme, _fileName: fileName },
1087
+ colorsAndType: resolvedColorsAndType && {
1088
+ ...resolvedColorsAndType.data,
1089
+ _fileName: fileName,
1090
+ _source: resolvedColorsAndType.source
1091
+ },
1092
+ componentConfigs: resolvedConfigs,
1093
+ skippedComponents
1575
1094
  });
1576
1095
  }
1577
- async function handleExportManifest({ params, res }) {
1096
+ async function handleExportTheme({ params, res }) {
1578
1097
  const [fileName] = params;
1579
- const manifest = manifestsResource.readJson(fileName);
1580
- if (!manifest) {
1581
- jsonResponse(res, 404, { error: "Manifest not found" });
1582
- return;
1583
- }
1584
- const themeName = sanitizeFileName(manifest.theme || "default");
1585
- const theme = themesResource.readJson(themeName);
1586
- if (!theme) {
1587
- jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
1098
+ if (rejectReservedName(res, fileName)) return;
1099
+ const read = readTheme(fileName);
1100
+ if (!read) {
1101
+ respondUnreadableTheme(res, fileName, "Theme not found");
1588
1102
  return;
1589
1103
  }
1590
- const knownComponents = new Set(listComponentNames());
1591
- const componentConfigs = {};
1592
- for (const [comp, configFile] of Object.entries(manifest.componentConfigs ?? {})) {
1593
- if (!knownComponents.has(comp)) continue;
1594
- const sanitized = sanitizeFileName(String(configFile) || "default");
1595
- if (sanitized === "default") continue;
1596
- const cfg = componentResource(comp).readJson(sanitized);
1597
- if (!cfg) {
1598
- jsonResponse(res, 422, {
1599
- error: `Manifest references missing config: ${comp}/${sanitized}`
1600
- });
1601
- return;
1602
- }
1603
- componentConfigs[`${comp}/${sanitized}`] = cfg;
1604
- }
1605
1104
  const bundle = {
1606
- kind: "manifest-bundle",
1607
- schemaVersion: 1,
1105
+ kind: "theme-bundle",
1106
+ schemaVersion: THEME_SCHEMA_VERSION,
1608
1107
  liveTokensVersion: PKG_VERSION,
1609
1108
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1610
- manifest,
1611
- theme,
1612
- componentConfigs
1109
+ manifest: read.theme
1613
1110
  };
1614
1111
  res.statusCode = 200;
1615
1112
  res.setHeader("Content-Type", "application/json");
@@ -1622,7 +1119,7 @@ ${lines.join("\n")}
1622
1119
  function nextAvailableName2(exists, baseName) {
1623
1120
  return nextAvailableName(exists, sanitizeFileName(baseName));
1624
1121
  }
1625
- async function handleImportManifest({ req, res }) {
1122
+ async function handleImportTheme({ req, res }) {
1626
1123
  let bundle;
1627
1124
  try {
1628
1125
  bundle = JSON.parse(await readBody(req));
@@ -1630,117 +1127,112 @@ ${lines.join("\n")}
1630
1127
  jsonResponse(res, 400, { error: "Body is not valid JSON" });
1631
1128
  return;
1632
1129
  }
1633
- if (!bundle || bundle.kind !== "manifest-bundle") {
1130
+ if (!bundle || !Object.values(BUNDLE_KIND_BY_VERSION).includes(bundle.kind)) {
1634
1131
  jsonResponse(res, 400, {
1635
- error: "Not a manifest bundle (kind discriminator missing or wrong)"
1132
+ error: "Not a theme bundle (kind discriminator missing or wrong)"
1636
1133
  });
1637
1134
  return;
1638
1135
  }
1639
- if (bundle.schemaVersion !== 1) {
1136
+ if (bundle.kind !== BUNDLE_KIND_BY_VERSION[bundle.schemaVersion]) {
1640
1137
  jsonResponse(res, 400, {
1641
- error: `Unsupported bundle schemaVersion: ${bundle.schemaVersion}`
1138
+ error: `Unsupported bundle: kind "${bundle.kind}" with schemaVersion ${bundle.schemaVersion}`
1642
1139
  });
1643
1140
  return;
1644
1141
  }
1645
- if (!bundle.manifest || !bundle.theme || !bundle.componentConfigs) {
1646
- jsonResponse(res, 400, { error: "Bundle missing manifest / theme / componentConfigs" });
1142
+ if (!bundle.manifest) {
1143
+ jsonResponse(res, 400, { error: "Bundle missing theme" });
1144
+ return;
1145
+ }
1146
+ const bundleResolvers = {
1147
+ readColorsAndType: () => bundle.theme,
1148
+ readComponentConfig: (comp, name) => bundle.componentConfigs?.[`${comp}/${name}`],
1149
+ normalizeColorsAndType: (colorsAndType) => normalizeColorsAndType(colorsAndType)
1150
+ };
1151
+ const { theme, dropped } = normalizeTheme(bundle.manifest, bundleResolvers);
1152
+ if (theme.colorsAndType) theme.colorsAndType = normalizeColorsAndType(theme.colorsAndType);
1153
+ if (!theme.colorsAndType) {
1154
+ jsonResponse(res, 400, { error: "Bundle carries no theme" });
1647
1155
  return;
1648
1156
  }
1157
+ theme.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1649
1158
  const renames = {};
1650
- const now = (/* @__PURE__ */ new Date()).toISOString();
1651
- const originalThemeName = sanitizeFileName(bundle.manifest.theme || "default");
1652
- const finalThemeName = nextAvailableName2(
1159
+ const originalName = sanitizeFileName(theme.name || "imported");
1160
+ const finalName = nextAvailableName2(
1653
1161
  (n) => themesResource.existingPath(n) !== null,
1654
- originalThemeName
1162
+ originalName
1655
1163
  );
1656
- if (finalThemeName !== originalThemeName) {
1657
- renames[`theme:${originalThemeName}`] = finalThemeName;
1164
+ if (finalName !== originalName) {
1165
+ renames[`theme:${originalName}`] = finalName;
1658
1166
  }
1659
- const themeBody = { ...bundle.theme };
1660
- themeBody.updatedAt = now;
1661
- if (!themeBody.createdAt) themeBody.createdAt = now;
1662
- themesResource.ensureDir();
1663
- fs2.writeFileSync(themesResource.filePath(finalThemeName), JSON.stringify(themeBody, null, 2));
1664
- const knownComponents = new Set(listComponentNames());
1665
- const componentRenames = {};
1666
- for (const [key, cfgValue] of Object.entries(bundle.componentConfigs)) {
1667
- const [comp, originalName] = key.split("/");
1668
- if (!comp || !originalName) continue;
1669
- if (!knownComponents.has(comp)) continue;
1670
- const r = componentResource(comp);
1671
- const finalName = nextAvailableName2(
1672
- (n) => r.existingPath(n) !== null,
1673
- originalName
1674
- );
1675
- if (finalName !== originalName) {
1676
- renames[`componentConfig:${comp}/${originalName}`] = finalName;
1677
- }
1678
- componentRenames[`${comp}/${originalName}`] = finalName;
1679
- const cfgBody = { ...cfgValue };
1680
- cfgBody.component = comp;
1681
- cfgBody.name = finalName;
1682
- cfgBody.updatedAt = now;
1683
- if (!cfgBody.createdAt) cfgBody.createdAt = now;
1684
- r.ensureDir();
1685
- fs2.writeFileSync(r.filePath(finalName), JSON.stringify(cfgBody, null, 2));
1167
+ writeTheme(finalName, theme);
1168
+ jsonResponse(res, 200, {
1169
+ ok: true,
1170
+ theme: finalName,
1171
+ renames,
1172
+ dropped
1173
+ });
1174
+ }
1175
+ async function handleAdoptLook({ res }) {
1176
+ const activeName = themesResource.getActiveName();
1177
+ if (activeName === "default") {
1178
+ jsonResponse(res, 409, {
1179
+ error: "Active theme is protected. Save As first.",
1180
+ code: "ACTIVE_IS_PROTECTED"
1181
+ });
1182
+ return;
1686
1183
  }
1687
- const rewrittenManifest = {
1688
- ...bundle.manifest,
1689
- theme: finalThemeName,
1690
- componentConfigs: {}
1691
- };
1692
- for (const [comp, configName] of Object.entries(bundle.manifest.componentConfigs ?? {})) {
1693
- const original = String(configName);
1694
- if (original === "default") {
1695
- rewrittenManifest.componentConfigs[comp] = "default";
1696
- continue;
1697
- }
1698
- const finalName = componentRenames[`${comp}/${original}`] ?? original;
1699
- rewrittenManifest.componentConfigs[comp] = finalName;
1700
- }
1701
- rewrittenManifest.updatedAt = now;
1702
- if (!rewrittenManifest.createdAt) rewrittenManifest.createdAt = now;
1703
- const originalManifestName = sanitizeFileName(bundle.manifest.name || "imported");
1704
- const finalManifestName = nextAvailableName2(
1705
- (n) => manifestsResource.existingPath(n) !== null,
1706
- originalManifestName
1707
- );
1708
- if (finalManifestName !== originalManifestName) {
1709
- renames[`manifest:${originalManifestName}`] = finalManifestName;
1184
+ const read = readTheme(activeName);
1185
+ if (!read) {
1186
+ respondUnreadableTheme(res, activeName, "Active theme not found");
1187
+ return;
1710
1188
  }
1711
- manifestsResource.ensureDir();
1712
- fs2.writeFileSync(
1713
- manifestsResource.filePath(finalManifestName),
1714
- JSON.stringify(rewrittenManifest, null, 2)
1715
- );
1189
+ themesResource.setProductionName(activeName);
1190
+ regenerateTokensCss();
1191
+ syncFontsToCss(read.theme.colorsAndType);
1716
1192
  jsonResponse(res, 200, {
1717
1193
  ok: true,
1718
- manifest: finalManifestName,
1719
- renames
1194
+ productionTheme: {
1195
+ fileName: activeName,
1196
+ name: read.theme.name,
1197
+ updatedAt: read.theme.updatedAt
1198
+ }
1720
1199
  });
1721
1200
  }
1722
1201
  function methodNotAllowed({ res }) {
1723
1202
  jsonResponse(res, 405, { error: "Method not allowed" });
1724
1203
  }
1204
+ let legacyLayout = null;
1725
1205
  const routes = [
1726
- // Themes — list / active / production are exact strings, must run before THEME_BY_NAME_REGEX
1727
- { method: "GET", pattern: THEMES_ROUTE, handler: handleListThemes },
1728
- { method: "GET", pattern: THEMES_ACTIVE_ROUTE, handler: handleGetActiveTheme },
1729
- { method: "PUT", pattern: THEMES_ACTIVE_ROUTE, handler: handleSetActiveTheme },
1730
- { method: "GET", pattern: THEMES_PRODUCTION_ROUTE, handler: handleGetProductionTheme },
1731
- { method: "PUT", pattern: THEMES_PRODUCTION_ROUTE, handler: handleSetProductionTheme },
1206
+ // Colors and type — list / active / production / working are exact strings;
1207
+ // they must run before COLORS_AND_TYPE_BY_NAME_REGEX
1208
+ { method: "GET", pattern: COLORS_AND_TYPE_ROUTE, handler: handleListColorsAndType },
1209
+ { method: "GET", pattern: COLORS_AND_TYPE_ACTIVE_ROUTE, handler: handleGetActiveColorsAndType },
1210
+ { method: "PUT", pattern: COLORS_AND_TYPE_ACTIVE_ROUTE, handler: methodNotAllowed },
1211
+ { method: "GET", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
1212
+ { method: "PUT", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
1213
+ { method: "POST", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
1214
+ { method: "DELETE", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
1215
+ { method: "GET", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: handleGetWorkingColorsAndType },
1216
+ { method: "PUT", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: handleSetWorkingColorsAndType },
1217
+ { method: "DELETE", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: handleClearWorkingColorsAndType },
1218
+ { method: "POST", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: methodNotAllowed },
1732
1219
  // Component configs — list of components
1733
1220
  { method: "GET", pattern: COMPONENT_CONFIGS_ROUTE, handler: handleListComponents },
1734
1221
  // Component configs — :comp/active (must precede :comp/:name)
1735
1222
  { method: "GET", pattern: COMP_ACTIVE_REGEX, handler: handleGetComponentActive },
1736
- { method: "PUT", pattern: COMP_ACTIVE_REGEX, handler: handleSetComponentActive },
1223
+ { method: "PUT", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
1737
1224
  { method: "POST", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
1738
1225
  { method: "DELETE", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
1739
- // Component configs — :comp/production (must precede :comp/:name)
1740
- { method: "GET", pattern: COMP_PRODUCTION_REGEX, handler: handleGetComponentProduction },
1741
- { method: "PUT", pattern: COMP_PRODUCTION_REGEX, handler: handleSetComponentProduction },
1226
+ // Component configs — :comp/production, retired with the per-layer pointers
1227
+ { method: "GET", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
1228
+ { method: "PUT", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
1742
1229
  { method: "POST", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
1743
1230
  { method: "DELETE", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
1231
+ // Component configs — :comp/working (must precede :comp/:name)
1232
+ { method: "GET", pattern: COMP_WORKING_REGEX, handler: handleGetComponentWorking },
1233
+ { method: "PUT", pattern: COMP_WORKING_REGEX, handler: handleSetComponentWorking },
1234
+ { method: "DELETE", pattern: COMP_WORKING_REGEX, handler: handleClearComponentWorking },
1235
+ { method: "POST", pattern: COMP_WORKING_REGEX, handler: methodNotAllowed },
1744
1236
  // Component configs — :comp/:name (must precede :comp listing)
1745
1237
  { method: "GET", pattern: COMP_BY_NAME_REGEX, handler: handleComponentConfigByName },
1746
1238
  { method: "PUT", pattern: COMP_BY_NAME_REGEX, handler: handleComponentConfigByName },
@@ -1748,33 +1240,44 @@ ${lines.join("\n")}
1748
1240
  { method: "POST", pattern: COMP_BY_NAME_REGEX, handler: methodNotAllowed },
1749
1241
  // Component configs — list configs for :comp (broadest, runs last among comp routes)
1750
1242
  { method: "GET", pattern: COMP_LIST_REGEX, handler: handleListComponentConfigs },
1243
+ // Colors and type — :name CRUD (broadest colors-and-type route, runs last)
1244
+ { method: "GET", pattern: COLORS_AND_TYPE_BY_NAME_REGEX, handler: handleColorsAndTypeByName },
1245
+ { method: "PUT", pattern: COLORS_AND_TYPE_BY_NAME_REGEX, handler: handleColorsAndTypeByName },
1246
+ { method: "DELETE", pattern: COLORS_AND_TYPE_BY_NAME_REGEX, handler: handleColorsAndTypeByName },
1247
+ // Themes — list / active / production are exact strings, must run before regexes
1248
+ { method: "GET", pattern: THEMES_ROUTE, handler: handleListThemes },
1249
+ { method: "GET", pattern: THEMES_ACTIVE_ROUTE, handler: handleGetActiveTheme },
1250
+ { method: "PUT", pattern: THEMES_ACTIVE_ROUTE, handler: handleSetActiveTheme },
1251
+ { method: "GET", pattern: THEMES_PRODUCTION_ROUTE, handler: handleGetProductionTheme },
1252
+ // Adopt is the one door that moves the production pointer; naming a theme
1253
+ // here would be a second, silent way to publish.
1254
+ { method: "PUT", pattern: THEMES_PRODUCTION_ROUTE, handler: methodNotAllowed },
1255
+ { method: "POST", pattern: THEMES_PRODUCTION_ROUTE, handler: methodNotAllowed },
1256
+ { method: "DELETE", pattern: THEMES_PRODUCTION_ROUTE, handler: methodNotAllowed },
1257
+ // Themes — exact import route runs before :name regexes
1258
+ { method: "POST", pattern: THEME_IMPORT_ROUTE, handler: handleImportTheme },
1259
+ { method: "PUT", pattern: THEME_IMPORT_ROUTE, handler: methodNotAllowed },
1260
+ { method: "GET", pattern: THEME_IMPORT_ROUTE, handler: methodNotAllowed },
1261
+ { method: "DELETE", pattern: THEME_IMPORT_ROUTE, handler: methodNotAllowed },
1262
+ // Themes — :name/apply (more specific than :name)
1263
+ { method: "PUT", pattern: THEME_APPLY_REGEX, handler: handleApplyTheme },
1264
+ { method: "POST", pattern: THEME_APPLY_REGEX, handler: methodNotAllowed },
1265
+ { method: "GET", pattern: THEME_APPLY_REGEX, handler: methodNotAllowed },
1266
+ { method: "DELETE", pattern: THEME_APPLY_REGEX, handler: methodNotAllowed },
1267
+ // Themes — :name/export (more specific than :name)
1268
+ { method: "GET", pattern: THEME_EXPORT_REGEX, handler: handleExportTheme },
1269
+ { method: "PUT", pattern: THEME_EXPORT_REGEX, handler: methodNotAllowed },
1270
+ { method: "POST", pattern: THEME_EXPORT_REGEX, handler: methodNotAllowed },
1271
+ { method: "DELETE", pattern: THEME_EXPORT_REGEX, handler: methodNotAllowed },
1751
1272
  // Themes — :name CRUD (broadest theme route, runs last)
1752
1273
  { method: "GET", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
1753
1274
  { method: "PUT", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
1754
1275
  { method: "DELETE", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
1755
- // Manifests — list / active are exact strings, must run before regexes
1756
- { method: "GET", pattern: MANIFESTS_ROUTE, handler: handleListManifests },
1757
- { method: "GET", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleGetActiveManifest },
1758
- { method: "PUT", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleSetActiveManifest },
1759
- // Manifests — exact import route runs before :name regexes
1760
- { method: "POST", pattern: MANIFEST_IMPORT_ROUTE, handler: handleImportManifest },
1761
- { method: "PUT", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
1762
- { method: "GET", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
1763
- { method: "DELETE", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
1764
- // Manifests — :name/apply (more specific than :name)
1765
- { method: "PUT", pattern: MANIFEST_APPLY_REGEX, handler: handleApplyManifest },
1766
- { method: "POST", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
1767
- { method: "GET", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
1768
- { method: "DELETE", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
1769
- // Manifests — :name/export (more specific than :name)
1770
- { method: "GET", pattern: MANIFEST_EXPORT_REGEX, handler: handleExportManifest },
1771
- { method: "PUT", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
1772
- { method: "POST", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
1773
- { method: "DELETE", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
1774
- // Manifests — :name CRUD (broadest manifest route, runs last)
1775
- { method: "GET", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
1776
- { method: "PUT", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
1777
- { method: "DELETE", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName }
1276
+ // Production — the whole-look adopt door
1277
+ { method: "PUT", pattern: PRODUCTION_ROUTE, handler: handleAdoptLook },
1278
+ { method: "GET", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
1279
+ { method: "POST", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
1280
+ { method: "DELETE", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed }
1778
1281
  ];
1779
1282
  return {
1780
1283
  name: "theme-file-api",
@@ -1788,19 +1291,42 @@ ${lines.join("\n")}
1788
1291
  };
1789
1292
  },
1790
1293
  configureServer(server) {
1791
- ensureThemesDir();
1792
- ensureComponentConfigsDir();
1793
- ensureManifestsDir();
1794
- regenerateTokensCss();
1795
- if (opts.autoMigrate) autoMigrateAdditive((msg) => server.config.logger.info(msg));
1796
- warnOnTokenDrift((msg) => server.config.logger.warn(msg));
1294
+ legacyLayout = detectLegacyLayout({
1295
+ dataDir: dataDirs.dataDir,
1296
+ colorsAndTypeDir: COLORS_AND_TYPE_DIR,
1297
+ themesDir: THEMES_DIR,
1298
+ configuredManifestsDir: dataDirs.legacyManifestsDir
1299
+ });
1300
+ if (legacyLayout) {
1301
+ warnOnLegacyLayout(legacyLayout.evidence, (msg) => server.config.logger.warn(msg));
1302
+ } else {
1303
+ ensureColorsAndTypeDir();
1304
+ ensureComponentConfigsDir();
1305
+ const { productionPointerHeld } = ensureThemesDir((msg) => server.config.logger.warn(msg));
1306
+ if (!productionPointerHeld || !fs.existsSync(GENERATED_CSS_PATH)) regenerateTokensCss();
1307
+ if (opts.autoMigrate) autoMigrateAdditive((msg) => server.config.logger.info(msg));
1308
+ warnOnTokenDrift((msg) => server.config.logger.warn(msg));
1309
+ warnOnLegacyPointers(
1310
+ listLegacyPointers(),
1311
+ productionPointerHeld,
1312
+ (msg) => server.config.logger.warn(msg)
1313
+ );
1314
+ }
1797
1315
  server.middlewares.use(async (req, res, next) => {
1316
+ if (legacyLayout && req.method !== "GET" && (req.url ?? "").startsWith(API_BASE)) {
1317
+ jsonResponse(res, 409, {
1318
+ error: "This project still uses the data layout from 0.47 and earlier. Run `npx live-tokens migrate`, then restart the dev server.",
1319
+ code: "LEGACY_LAYOUT"
1320
+ });
1321
+ return;
1322
+ }
1798
1323
  const handled = await dispatch(req, res, routes);
1799
1324
  if (!handled) next();
1800
1325
  });
1801
1326
  },
1802
1327
  handleHotUpdate(ctx) {
1803
- const normalized = path2.resolve(ctx.file);
1328
+ if (legacyLayout) return;
1329
+ const normalized = path.resolve(ctx.file);
1804
1330
  if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
1805
1331
  if (!normalized.endsWith(".svelte")) return;
1806
1332
  if (!isThemeAwareComponent(normalized)) return;