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