@motion-proto/live-tokens 0.7.1 → 0.9.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 (96) hide show
  1. package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
  2. package/README.md +34 -0
  3. package/dist-plugin/index.cjs +707 -90
  4. package/dist-plugin/index.d.cts +1 -0
  5. package/dist-plugin/index.d.ts +1 -0
  6. package/dist-plugin/index.js +707 -90
  7. package/package.json +6 -2
  8. package/src/app/site.css +1 -1
  9. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
  10. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  11. package/src/editor/component-editor/NotificationEditor.svelte +3 -1
  12. package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
  13. package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
  14. package/src/editor/component-editor/editors.d.ts +10 -0
  15. package/src/editor/component-editor/index.ts +16 -1
  16. package/src/editor/component-editor/registry.ts +103 -26
  17. package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
  18. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
  19. package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
  20. package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
  21. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  22. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
  23. package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
  24. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
  25. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
  26. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
  27. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
  28. package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
  29. package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
  30. package/src/editor/component-editor/scaffolding/types.ts +11 -0
  31. package/src/editor/core/components/componentConfigKeys.ts +22 -3
  32. package/src/editor/core/components/componentConfigService.ts +2 -2
  33. package/src/editor/core/components/componentPersist.ts +7 -5
  34. package/src/editor/core/manifests/manifestService.ts +58 -3
  35. package/src/editor/core/palettes/familySwap.ts +99 -0
  36. package/src/editor/core/palettes/paletteDerivation.ts +69 -0
  37. package/src/editor/core/palettes/tokenRegistry.ts +4 -1
  38. package/src/editor/core/store/editorStore.ts +206 -12
  39. package/src/editor/core/store/editorTypes.ts +55 -12
  40. package/src/editor/core/store/gradientSource.ts +192 -0
  41. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  42. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  43. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  44. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  45. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  46. package/src/editor/core/themes/migrations/index.ts +10 -0
  47. package/src/editor/core/themes/slices/components.ts +27 -4
  48. package/src/editor/core/themes/slices/gradients.ts +88 -13
  49. package/src/editor/core/themes/themeInit.ts +2 -2
  50. package/src/editor/core/themes/themeTypes.ts +56 -1
  51. package/src/editor/index.ts +10 -1
  52. package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
  53. package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
  54. package/src/editor/pages/ComponentEditorPage.svelte +53 -3
  55. package/src/editor/pages/EditorShell.svelte +53 -3
  56. package/src/editor/styles/ui-editor.css +1 -0
  57. package/src/editor/styles/ui-form-controls.css +19 -20
  58. package/src/editor/ui/BezierCurveEditor.svelte +114 -63
  59. package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
  60. package/src/editor/ui/FileLoadList.svelte +22 -5
  61. package/src/editor/ui/FontStackEditor.svelte +214 -76
  62. package/src/editor/ui/GradientEditor.svelte +435 -215
  63. package/src/editor/ui/GradientStopPicker.svelte +11 -3
  64. package/src/editor/ui/ManifestFileManager.svelte +71 -4
  65. package/src/editor/ui/PaletteEditor.svelte +52 -79
  66. package/src/editor/ui/ProjectFontsSection.svelte +328 -293
  67. package/src/editor/ui/ThemeFileManager.svelte +0 -4
  68. package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
  69. package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
  70. package/src/editor/ui/UIInfoPopover.svelte +0 -1
  71. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  72. package/src/editor/ui/UIPaletteSelector.svelte +31 -4
  73. package/src/editor/ui/UIPillButton.svelte +33 -3
  74. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  75. package/src/editor/ui/UITokenSelector.svelte +4 -1
  76. package/src/editor/ui/VariablesTab.svelte +41 -35
  77. package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
  78. package/src/editor/ui/palette/PaletteBase.svelte +3 -3
  79. package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
  80. package/src/editor/ui/sections/GradientsSection.svelte +1 -1
  81. package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
  82. package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
  83. package/src/system/components/Button.svelte +2 -2
  84. package/src/system/components/Card.svelte +29 -1
  85. package/src/system/components/CollapsibleSection.svelte +25 -2
  86. package/src/system/components/Dialog.svelte +24 -4
  87. package/src/system/components/FloatingTokenTags.css +43 -24
  88. package/src/system/components/FloatingTokenTags.svelte +88 -137
  89. package/src/system/components/Notification.svelte +8 -1
  90. package/src/system/components/SectionDivider.svelte +532 -381
  91. package/src/system/styles/CONVENTIONS.md +1 -1
  92. package/src/system/styles/fonts.css +3 -16
  93. package/src/system/styles/tokens.css +356 -1199
  94. package/src/system/styles/tokens.generated.css +544 -0
  95. package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
  96. package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
@@ -18,6 +18,410 @@ function sanitizeFileName(name) {
18
18
  return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9\-_]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unnamed";
19
19
  }
20
20
 
21
+ // src/editor/core/palettes/oklch.ts
22
+ function srgbToLinear(c) {
23
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
24
+ }
25
+ function linearToSrgb(c) {
26
+ return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
27
+ }
28
+ function hexToLinearRgb(hex) {
29
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
30
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
31
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
32
+ return [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
33
+ }
34
+ function linearRgbToHex(r, g, b) {
35
+ const toHex = (c) => {
36
+ const v = Math.round(Math.max(0, Math.min(1, linearToSrgb(c))) * 255);
37
+ return v.toString(16).padStart(2, "0");
38
+ };
39
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
40
+ }
41
+ function linearRgbToOklab(r, g, b) {
42
+ const l_ = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
43
+ const m_ = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
44
+ const s_ = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
45
+ const l = Math.cbrt(l_);
46
+ const m = Math.cbrt(m_);
47
+ const s = Math.cbrt(s_);
48
+ return [
49
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
50
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
51
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
52
+ ];
53
+ }
54
+ function oklabToLinearRgb(L, a, b) {
55
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
56
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
57
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
58
+ const l = l_ * l_ * l_;
59
+ const m = m_ * m_ * m_;
60
+ const s = s_ * s_ * s_;
61
+ return [
62
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
63
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
64
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
65
+ ];
66
+ }
67
+ function oklabToOklch(L, a, b) {
68
+ const c = Math.sqrt(a * a + b * b);
69
+ let h = Math.atan2(b, a) * 180 / Math.PI;
70
+ if (h < 0) h += 360;
71
+ return { l: L, c, h };
72
+ }
73
+ function oklchToOklab(l, c, h) {
74
+ const hRad = h * Math.PI / 180;
75
+ return [l, c * Math.cos(hRad), c * Math.sin(hRad)];
76
+ }
77
+ function hexToOklch(hex) {
78
+ const [r, g, b] = hexToLinearRgb(hex);
79
+ const [L, a, bVal] = linearRgbToOklab(r, g, b);
80
+ return oklabToOklch(L, a, bVal);
81
+ }
82
+ function oklchToHex(l, c, h) {
83
+ const [L, a, b] = oklchToOklab(l, c, h);
84
+ const [r, g, bVal] = oklabToLinearRgb(L, a, b);
85
+ return linearRgbToHex(r, g, bVal);
86
+ }
87
+ function isInGamut(r, g, b) {
88
+ const eps = 1e-4;
89
+ return r >= -eps && r <= 1 + eps && g >= -eps && g <= 1 + eps && b >= -eps && b <= 1 + eps;
90
+ }
91
+ function gamutClamp(l, c, h) {
92
+ if (l <= 0) return { l: 0, c: 0, h };
93
+ if (l >= 1) return { l: 1, c: 0, h };
94
+ const [L, a, b] = oklchToOklab(l, c, h);
95
+ const [r, g, bVal] = oklabToLinearRgb(L, a, b);
96
+ if (isInGamut(r, g, bVal)) return { l, c, h };
97
+ let lo = 0;
98
+ let hi = c;
99
+ for (let i = 0; i < 20; i++) {
100
+ const mid = (lo + hi) / 2;
101
+ const [La, aa, ba] = oklchToOklab(l, mid, h);
102
+ const [rr, gg, bb] = oklabToLinearRgb(La, aa, ba);
103
+ if (isInGamut(rr, gg, bb)) {
104
+ lo = mid;
105
+ } else {
106
+ hi = mid;
107
+ }
108
+ }
109
+ return { l, c: lo, h };
110
+ }
111
+
112
+ // src/editor/ui/curveEngine.ts
113
+ function makeAnchor(x, y, tangentLen = 15) {
114
+ return { x, y, inDx: -tangentLen, inDy: 0, outDx: tangentLen, outDy: 0 };
115
+ }
116
+ function evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, t) {
117
+ const u = 1 - t, u2 = u * u, u3 = u2 * u;
118
+ const t2 = t * t, t3 = t2 * t;
119
+ return {
120
+ x: u3 * p0x + 3 * u2 * t * c0x + 3 * u * t2 * c1x + t3 * p1x,
121
+ y: u3 * p0y + 3 * u2 * t * c0y + 3 * u * t2 * c1y + t3 * p1y
122
+ };
123
+ }
124
+ function sampleCurve(anchors, xPos) {
125
+ if (anchors.length === 0) return 0;
126
+ if (anchors.length === 1) return anchors[0].y;
127
+ if (xPos <= anchors[0].x) return anchors[0].y;
128
+ if (xPos >= anchors[anchors.length - 1].x) return anchors[anchors.length - 1].y;
129
+ let seg = 0;
130
+ while (seg < anchors.length - 2 && anchors[seg + 1].x < xPos) seg++;
131
+ const a0 = anchors[seg], a1 = anchors[seg + 1];
132
+ const p0x = a0.x, p0y = a0.y;
133
+ const c0x = a0.x + a0.outDx, c0y = a0.y + a0.outDy;
134
+ const c1x = a1.x + a1.inDx, c1y = a1.y + a1.inDy;
135
+ const p1x = a1.x, p1y = a1.y;
136
+ let lo = 0, hi = 1;
137
+ for (let i = 0; i < 20; i++) {
138
+ const mid = (lo + hi) / 2;
139
+ const pt = evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, mid);
140
+ if (pt.x < xPos) lo = mid;
141
+ else hi = mid;
142
+ }
143
+ return evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, (lo + hi) / 2).y;
144
+ }
145
+
146
+ // src/editor/core/palettes/paletteDerivation.ts
147
+ var PALETTE_SPECS = [
148
+ { label: "Neutral", cssNamespace: "neutral", mode: "gray", initialColor: "#808080" },
149
+ { label: "Alternate", cssNamespace: "alternate", mode: "gray", initialColor: "#808080" },
150
+ { label: "Background", cssNamespace: "canvas", mode: "chromatic", emptySelector: true, initialColor: "#1a1a2e" },
151
+ { label: "Brand", cssNamespace: "brand", mode: "chromatic", initialColor: "#c93636" },
152
+ { label: "Accent", cssNamespace: "accent", mode: "chromatic", initialColor: "#f49e0b" },
153
+ { label: "Special", cssNamespace: "special", mode: "chromatic", initialColor: "#8b5cf6" },
154
+ { label: "Info", cssNamespace: "info", mode: "chromatic", initialColor: "#3077e8" },
155
+ { label: "Success", cssNamespace: "success", mode: "chromatic", initialColor: "#21c45d" },
156
+ { label: "Warning", cssNamespace: "warning", mode: "chromatic", initialColor: "#e66e1a" },
157
+ { label: "Danger", cssNamespace: "danger", mode: "chromatic", initialColor: "#e8304f" }
158
+ ];
159
+ var PALETTE_STEPS = [
160
+ { label: "100" },
161
+ { label: "200" },
162
+ { label: "300" },
163
+ { label: "400" },
164
+ { label: "500" },
165
+ { label: "600" },
166
+ { label: "700" },
167
+ { label: "800" },
168
+ { label: "850" },
169
+ { label: "900" },
170
+ { label: "950" }
171
+ ];
172
+ var GRAY_STEPS = [
173
+ { label: "100" },
174
+ { label: "200" },
175
+ { label: "300" },
176
+ { label: "400" },
177
+ { label: "500" },
178
+ { label: "600" },
179
+ { label: "700" },
180
+ { label: "800" },
181
+ { label: "850" },
182
+ { label: "900" },
183
+ { label: "950" }
184
+ ];
185
+ var SCALES = [
186
+ {
187
+ title: "Surfaces",
188
+ isText: false,
189
+ steps: [
190
+ { name: "lowest", position: -1 },
191
+ { name: "lower", position: -2 / 3 },
192
+ { name: "low", position: -1 / 3 },
193
+ { name: "default", position: 0 },
194
+ { name: "high", position: 1 / 3 },
195
+ { name: "higher", position: 2 / 3 },
196
+ { name: "highest", position: 1 }
197
+ ]
198
+ },
199
+ {
200
+ title: "Borders",
201
+ isText: false,
202
+ steps: [
203
+ { name: "faint", position: -1 },
204
+ { name: "subtle", position: -0.5 },
205
+ { name: "default", position: 0 },
206
+ { name: "medium", position: 0.5 },
207
+ { name: "strong", position: 1 }
208
+ ]
209
+ },
210
+ {
211
+ title: "Text",
212
+ isText: true,
213
+ steps: [
214
+ { name: "primary", position: 0 },
215
+ { name: "secondary", position: 0 },
216
+ { name: "tertiary", position: 0 },
217
+ { name: "muted", position: 0 },
218
+ { name: "disabled", position: 0 }
219
+ ]
220
+ }
221
+ ];
222
+ var DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
223
+ var DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
224
+ var DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
225
+ var DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
226
+ var DEFAULT_TINT_CHROMA = 0.04;
227
+ var defaultScaleCurves = {
228
+ Surfaces: {
229
+ lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
230
+ saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)]
231
+ },
232
+ Borders: {
233
+ lightness: () => [makeAnchor(0, 25, 5), makeAnchor(100, 80, 5)],
234
+ saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)]
235
+ },
236
+ Text: {
237
+ lightness: () => [makeAnchor(0, 120, 30), makeAnchor(100, 55, 30)],
238
+ saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 15, 30)]
239
+ }
240
+ };
241
+ function paletteStepKey(label) {
242
+ return `Palette-${label}`;
243
+ }
244
+ function grayStepKey(label) {
245
+ return `gray-${label}`;
246
+ }
247
+ function stepKey(scaleTitle, stepName) {
248
+ return `${scaleTitle}-${stepName}`;
249
+ }
250
+ function stepIndexToX(index, total) {
251
+ return total > 1 ? index / (total - 1) * 100 : 50;
252
+ }
253
+ function computePaletteColor(index, base, lightnessCurve, saturationCurve, curveOffset) {
254
+ const { c: baseC, h } = hexToOklch(base);
255
+ const xPos = stepIndexToX(index, PALETTE_STEPS.length);
256
+ const targetL = Math.max(0, Math.min(100, sampleCurve(lightnessCurve, xPos) + (curveOffset.lightness ?? 0))) / 100;
257
+ const satMul = Math.max(0, Math.min(2, (sampleCurve(saturationCurve, xPos) + (curveOffset.saturation ?? 0)) / 100));
258
+ const targetC = baseC * satMul;
259
+ const clamped = gamutClamp(targetL, targetC, h);
260
+ return oklchToHex(clamped.l, clamped.c, clamped.h);
261
+ }
262
+ function computeGrayColor(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset) {
263
+ const xPos = stepIndexToX(index, GRAY_STEPS.length);
264
+ const lOff = curveOffset["gray-lightness"] ?? 0;
265
+ const sOff = curveOffset["gray-saturation"] ?? 0;
266
+ const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
267
+ const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
268
+ const targetC = chroma * satMul;
269
+ const clamped = gamutClamp(targetL, targetC, hue);
270
+ return oklchToHex(clamped.l, clamped.c, clamped.h);
271
+ }
272
+ function computeDerivedColor(step, base, scaleTitle, scaleCurves, curveOffset) {
273
+ const scale = SCALES.find((s) => s.title === scaleTitle);
274
+ const idx = scale.steps.indexOf(step);
275
+ const xPos = stepIndexToX(idx, scale.steps.length);
276
+ const defs = defaultScaleCurves[scaleTitle];
277
+ const lCurve = scaleCurves[scaleTitle]?.lightness ?? defs.lightness();
278
+ const sCurve = scaleCurves[scaleTitle]?.saturation ?? defs.saturation();
279
+ const lOff = curveOffset[`${scaleTitle}-lightness`] ?? 0;
280
+ const sOff = curveOffset[`${scaleTitle}-saturation`] ?? 0;
281
+ const { l: baseL, c: baseC, h: baseH } = hexToOklch(base);
282
+ let targetL;
283
+ if (scale.isText) {
284
+ const lMul = Math.max(0, Math.min(2, (sampleCurve(lCurve, xPos) + lOff) / 100));
285
+ targetL = Math.max(0, Math.min(1, baseL * lMul));
286
+ } else {
287
+ targetL = Math.max(0, Math.min(100, sampleCurve(lCurve, xPos) + lOff)) / 100;
288
+ }
289
+ const satMul = Math.max(0, Math.min(2, (sampleCurve(sCurve, xPos) + sOff) / 100));
290
+ const targetC = baseC * satMul;
291
+ const clamped = gamutClamp(targetL, targetC, baseH);
292
+ return oklchToHex(clamped.l, clamped.c, clamped.h);
293
+ }
294
+ function scaleToCssVar(scaleTitle, stepName, cssNamespace) {
295
+ if (cssNamespace === null) return null;
296
+ if (scaleTitle === "Surfaces") {
297
+ const suffix = stepName === "default" ? "" : `-${stepName}`;
298
+ return cssNamespace === "neutral" ? `--surface-neutral${suffix}` : `--surface-${cssNamespace}${suffix}`;
299
+ }
300
+ if (scaleTitle === "Borders") {
301
+ const suffix = stepName === "default" ? "" : `-${stepName}`;
302
+ return cssNamespace === "neutral" ? `--border-neutral${suffix}` : `--border-${cssNamespace}${suffix}`;
303
+ }
304
+ if (scaleTitle === "Text") {
305
+ if (cssNamespace === "neutral") return `--text-${stepName}`;
306
+ return stepName === "primary" ? `--text-${cssNamespace}` : `--text-${cssNamespace}-${stepName}`;
307
+ }
308
+ return null;
309
+ }
310
+ function derivePaletteVars(spec, config) {
311
+ const out = {};
312
+ if (!config) return out;
313
+ const baseColor = config.baseColor ?? spec.initialColor;
314
+ const overrides = config.overrides ?? {};
315
+ const curveOffset = config.curveOffset ?? {};
316
+ const scaleCurves = config.scaleCurves ?? {};
317
+ let baseForScales;
318
+ if (spec.mode === "gray") {
319
+ const grayLightnessCurve = config.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS();
320
+ const graySaturationCurve = config.graySaturationCurve ?? DEFAULT_GRAY_SATURATION();
321
+ const tintHue = config.tintHue ?? 240;
322
+ const tintChroma = config.tintChroma ?? DEFAULT_TINT_CHROMA;
323
+ let gray500 = "#808080";
324
+ GRAY_STEPS.forEach((step, index) => {
325
+ const k = grayStepKey(step.label);
326
+ const hex = computeGrayColor(index, tintHue, tintChroma, grayLightnessCurve, graySaturationCurve, curveOffset);
327
+ const effective = k in overrides ? overrides[k] : hex;
328
+ out[`--color-${spec.cssNamespace}-${step.label}`] = effective;
329
+ if (step.label === "500") gray500 = hex;
330
+ });
331
+ baseForScales = gray500;
332
+ } else {
333
+ const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
334
+ const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
335
+ PALETTE_STEPS.forEach((ps, index) => {
336
+ const k = paletteStepKey(ps.label);
337
+ const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
338
+ const effective = k in overrides ? overrides[k] : hex;
339
+ out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
340
+ });
341
+ baseForScales = baseColor;
342
+ }
343
+ for (const scale of SCALES) {
344
+ for (const step of scale.steps) {
345
+ const k = stepKey(scale.title, step.name);
346
+ const hex = k in overrides ? overrides[k] : computeDerivedColor(step, baseForScales, scale.title, scaleCurves, curveOffset);
347
+ const varName = scaleToCssVar(scale.title, step.name, spec.cssNamespace);
348
+ if (varName) out[varName] = hex;
349
+ }
350
+ }
351
+ if (spec.emptySelector) {
352
+ const emptyMode = config.emptyMode ?? "solid";
353
+ const emptyStep = config.emptyStep ?? "850";
354
+ const gradientStyle = config.gradientStyle ?? "linear";
355
+ const gradientAngle = config.gradientAngle ?? 180;
356
+ const gradientReverse = config.gradientReverse ?? false;
357
+ const gradientSize = config.gradientSize ?? "page";
358
+ const gradientStops = config.gradientStops ?? [
359
+ { position: 0, paletteLabel: "800" },
360
+ { position: 100, paletteLabel: "950" }
361
+ ];
362
+ if (emptyMode === "solid") {
363
+ const stepHex = out[`--color-${spec.cssNamespace}-${emptyStep}`];
364
+ if (stepHex) out["--page-bg"] = stepHex;
365
+ out["--page-bg-attachment"] = "scroll";
366
+ } else {
367
+ const sortedStops = [...gradientStops].sort(
368
+ (a, b) => gradientReverse ? b.position - a.position : a.position - b.position
369
+ );
370
+ const stopsCss = sortedStops.map((s) => `${out[`--color-${spec.cssNamespace}-${s.paletteLabel}`] ?? "#000000"} ${s.position}%`).join(", ");
371
+ let gradient;
372
+ switch (gradientStyle) {
373
+ case "radial":
374
+ gradient = `radial-gradient(circle, ${stopsCss})`;
375
+ break;
376
+ case "conic":
377
+ gradient = `conic-gradient(from ${gradientAngle}deg, ${stopsCss})`;
378
+ break;
379
+ default:
380
+ gradient = `linear-gradient(${gradientAngle}deg, ${stopsCss})`;
381
+ }
382
+ out["--page-bg"] = gradient;
383
+ out["--page-bg-attachment"] = gradientSize === "window" ? "fixed" : "scroll";
384
+ }
385
+ }
386
+ return out;
387
+ }
388
+ function palettesToVars(palettes) {
389
+ const out = {};
390
+ for (const spec of PALETTE_SPECS) {
391
+ Object.assign(out, derivePaletteVars(spec, palettes[spec.label]));
392
+ }
393
+ return out;
394
+ }
395
+ var HEX_RE = /^#[0-9a-f]{6}$/i;
396
+ function reconcilePalettesFromCssVars(palettes, cssVars) {
397
+ const next = structuredClone(palettes);
398
+ const consumed = /* @__PURE__ */ new Set();
399
+ const snapped = /* @__PURE__ */ new Set();
400
+ for (const spec of PALETTE_SPECS) {
401
+ const current = next[spec.label];
402
+ if (current === void 0) continue;
403
+ if (current._imported === true) {
404
+ const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
405
+ if (anchorHex && HEX_RE.test(anchorHex.trim())) {
406
+ const hex = anchorHex.trim();
407
+ if (spec.mode === "gray") {
408
+ const { c, h } = hexToOklch(hex);
409
+ next[spec.label] = { ...current, tintHue: h, tintChroma: c, _imported: false };
410
+ } else {
411
+ next[spec.label] = { ...current, baseColor: hex, _imported: false };
412
+ }
413
+ snapped.add(spec.label);
414
+ } else {
415
+ next[spec.label] = { ...current, _imported: false };
416
+ }
417
+ }
418
+ for (const k of Object.keys(derivePaletteVars(spec, next[spec.label]))) {
419
+ consumed.add(k);
420
+ }
421
+ }
422
+ return { palettes: next, consumed, snapped };
423
+ }
424
+
21
425
  // vite-plugin/files/versionedFileResourceServer.ts
22
426
  import fs from "fs";
23
427
  import path from "path";
@@ -106,6 +510,16 @@ async function dispatch(req, res, routes) {
106
510
  return false;
107
511
  }
108
512
 
513
+ // vite-plugin/files/nameAllocator.ts
514
+ function nextAvailableName(exists, baseName, maxAttempts = 1e3) {
515
+ if (!exists(baseName)) return baseName;
516
+ for (let i = 2; i < maxAttempts; i++) {
517
+ const candidate = `${baseName}-${i}`;
518
+ if (!exists(candidate)) return candidate;
519
+ }
520
+ throw new Error(`Could not allocate a non-colliding name for "${baseName}"`);
521
+ }
522
+
109
523
  // vite-plugin/themeFileApi.ts
110
524
  import { fileURLToPath } from "url";
111
525
  var PKG_VERSION = (() => {
@@ -128,6 +542,7 @@ var PKG_VERSION = (() => {
128
542
  function themeFileApi(opts) {
129
543
  const THEMES_DIR = path2.resolve(opts.themesDir);
130
544
  const CSS_PATH = path2.resolve(opts.tokensCssPath);
545
+ const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? path2.resolve(opts.tokensGeneratedCssPath) : path2.join(path2.dirname(CSS_PATH), "tokens.generated.css");
131
546
  const FONTS_CSS_PATH = opts.fontsCssPath ? path2.resolve(opts.fontsCssPath) : path2.join(path2.dirname(CSS_PATH), "fonts.css");
132
547
  const API_BASE = opts.apiBase ?? "/api";
133
548
  const COMPONENT_CONFIGS_DIR = opts.componentConfigsDir ? path2.resolve(opts.componentConfigsDir) : path2.resolve("component-configs");
@@ -174,6 +589,17 @@ function themeFileApi(opts) {
174
589
  res.setHeader("Content-Type", "application/json");
175
590
  res.end(JSON.stringify(data));
176
591
  }
592
+ function normalizeTheme(theme) {
593
+ if (!theme || typeof theme !== "object") return theme;
594
+ const palettes = theme.editorConfigs ?? {};
595
+ const cssVars = theme.cssVariables ?? {};
596
+ const { palettes: nextPalettes, consumed } = reconcilePalettesFromCssVars(palettes, cssVars);
597
+ const nextCssVars = {};
598
+ for (const [k, v] of Object.entries(cssVars)) {
599
+ if (!consumed.has(k)) nextCssVars[k] = v;
600
+ }
601
+ return { ...theme, editorConfigs: nextPalettes, cssVariables: nextCssVars };
602
+ }
177
603
  const SYSTEM_CASCADES_SSR = {
178
604
  "system-ui-sans": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
179
605
  "system-ui-serif": 'Georgia, "Times New Roman", serif',
@@ -205,42 +631,78 @@ function themeFileApi(opts) {
205
631
  }
206
632
  return out;
207
633
  }
208
- function syncTokensToCss(fileName) {
209
- const themePath = path2.join(THEMES_DIR, `${fileName}.json`);
210
- if (!fs2.existsSync(themePath)) return;
211
- const themeData = JSON.parse(fs2.readFileSync(themePath, "utf-8"));
212
- const cssVars = { ...themeData.cssVariables || {} };
213
- const resolvedFontVars = resolveFontStacks(themeData);
214
- for (const [name, value] of Object.entries(resolvedFontVars)) {
215
- cssVars[name] = value;
216
- }
217
- if (Object.keys(cssVars).length === 0) return;
218
- const cssContent = fs2.readFileSync(CSS_PATH, "utf-8");
219
- const remaining = new Set(Object.keys(cssVars));
220
- const updatedContent = cssContent.replace(
221
- /^(\s*)(--[\w-]+):\s*(.+);/gm,
222
- (_match, indent, varName, oldValue) => {
223
- if (cssVars[varName] !== void 0) {
224
- remaining.delete(varName);
225
- return `${indent}${varName}: ${cssVars[varName]};`;
634
+ function regenerateTokensCss() {
635
+ const lines = [];
636
+ lines.push("/* Generated by themeFileApi from the production theme and component configs. Do not edit. */");
637
+ lines.push("/* tokens.css holds developer-authored defaults; this file holds editor overrides. */");
638
+ lines.push("");
639
+ const productionThemeName = themesResource.getProductionName();
640
+ const themePath = path2.join(THEMES_DIR, `${productionThemeName}.json`);
641
+ let themeVarCount = 0;
642
+ if (fs2.existsSync(themePath)) {
643
+ const themeData = JSON.parse(fs2.readFileSync(themePath, "utf-8"));
644
+ const cssVars = { ...themeData.cssVariables || {} };
645
+ Object.assign(cssVars, palettesToVars(themeData.editorConfigs ?? {}));
646
+ const resolvedFontVars = resolveFontStacks(themeData);
647
+ for (const [name, value] of Object.entries(resolvedFontVars)) {
648
+ cssVars[name] = value;
649
+ }
650
+ themeVarCount = Object.keys(cssVars).length;
651
+ if (themeVarCount > 0) {
652
+ lines.push(`/* Production theme: ${productionThemeName} */`);
653
+ lines.push(":root:root {");
654
+ for (const [name, value] of Object.entries(cssVars)) {
655
+ lines.push(` ${name}: ${value};`);
226
656
  }
227
- return `${indent}${varName}: ${oldValue.trim()};`;
657
+ lines.push("}");
658
+ lines.push("");
228
659
  }
229
- );
230
- let finalContent = updatedContent;
231
- if (remaining.size > 0) {
232
- const newVars = [...remaining].map((name) => ` ${name}: ${cssVars[name]};`).join("\n");
233
- finalContent = finalContent.replace(
234
- /\n\}(\s*)$/,
235
- `
236
-
237
- /* Token additions */
238
- ${newVars}
239
- }$1`
240
- );
241
660
  }
242
- fs2.writeFileSync(CSS_PATH, finalContent);
243
- console.log(`[syncTokensToCss] Wrote ${Object.keys(cssVars).length} variables from "${fileName}" into tokens.css`);
661
+ let componentOverrideCount = 0;
662
+ if (fs2.existsSync(COMPONENT_CONFIGS_DIR)) {
663
+ const blocks = [];
664
+ for (const comp of listComponentNames()) {
665
+ const prod = componentResource(comp).getProductionName();
666
+ if (prod === "default") continue;
667
+ const prodCfg = readComponentConfig(comp, prod);
668
+ const defaultCfg = readComponentConfig(comp, "default");
669
+ if (!prodCfg || !defaultCfg) continue;
670
+ const overrides = [];
671
+ const defaultAliases = defaultCfg.aliases ?? {};
672
+ for (const [varName, semanticValue] of Object.entries(prodCfg.aliases ?? {})) {
673
+ if (!aliasValuesEqual(defaultAliases[varName], semanticValue)) {
674
+ overrides.push([varName, semanticValue]);
675
+ }
676
+ }
677
+ if (overrides.length === 0) continue;
678
+ const block = [` /* ${comp} (${prod}) */`];
679
+ for (const [varName, semanticValue] of overrides) {
680
+ block.push(` ${varName}: ${aliasValueToCss(semanticValue)};`);
681
+ }
682
+ blocks.push(block);
683
+ componentOverrideCount += overrides.length;
684
+ }
685
+ if (blocks.length > 0) {
686
+ lines.push("/* Component aliases (production configs differing from defaults) */");
687
+ lines.push(":root:root {");
688
+ for (let i = 0; i < blocks.length; i++) {
689
+ if (i > 0) lines.push("");
690
+ lines.push(...blocks[i]);
691
+ }
692
+ lines.push("}");
693
+ lines.push("");
694
+ }
695
+ }
696
+ if (!fs2.existsSync(path2.dirname(GENERATED_CSS_PATH))) {
697
+ fs2.mkdirSync(path2.dirname(GENERATED_CSS_PATH), { recursive: true });
698
+ }
699
+ fs2.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
700
+ console.log(
701
+ `[regenerateTokensCss] Wrote ${path2.basename(GENERATED_CSS_PATH)} (${themeVarCount} theme vars, ${componentOverrideCount} component overrides)`
702
+ );
703
+ }
704
+ function syncTokensToCss(_fileName) {
705
+ regenerateTokensCss();
244
706
  }
245
707
  function syncFontsToCss(fileName) {
246
708
  const themePath = path2.join(THEMES_DIR, `${fileName}.json`);
@@ -252,15 +714,20 @@ ${newVars}
252
714
  lines.push("/* Generated from the production theme by syncFontsToCss. Do not edit. */");
253
715
  lines.push("/* Both fonts.css and fonts/ are in dist/, so relative paths work at runtime. */");
254
716
  lines.push("");
255
- for (const source of sources) {
717
+ const urlSources = sources.filter((s) => s.kind !== "font-face" && s.url);
718
+ const faceSources = sources.filter((s) => s.kind === "font-face" && s.cssText);
719
+ for (const source of urlSources) {
256
720
  const familyNames = source.families.map((f) => f.name).join(", ");
257
721
  const label = source.label ? `${source.label} \u2014 ${familyNames}` : familyNames;
258
722
  lines.push(`/* ${label} */`);
259
- if (source.kind === "font-face") {
260
- if (source.cssText) lines.push(source.cssText);
261
- } else if (source.url) {
262
- lines.push(`@import url('${source.url}');`);
263
- }
723
+ lines.push(`@import url('${source.url}');`);
724
+ lines.push("");
725
+ }
726
+ for (const source of faceSources) {
727
+ const familyNames = source.families.map((f) => f.name).join(", ");
728
+ const label = source.label ? `${source.label} \u2014 ${familyNames}` : familyNames;
729
+ lines.push(`/* ${label} */`);
730
+ lines.push(source.cssText);
264
731
  lines.push("");
265
732
  }
266
733
  const content = lines.join("\n");
@@ -400,6 +867,33 @@ ${newVars}
400
867
  fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
401
868
  return true;
402
869
  }
870
+ function formatAliasGradient(v) {
871
+ const stopColor = (s) => {
872
+ const base = s.color.startsWith("--") ? `var(${s.color})` : s.color;
873
+ const opacity = s.opacity ?? 100;
874
+ return opacity >= 100 ? base : `color-mix(in srgb, ${base} ${opacity}%, transparent)`;
875
+ };
876
+ if (v.type === "none") return "transparent";
877
+ if (v.type === "solid") {
878
+ const first = v.stops[0];
879
+ if (!first) return "transparent";
880
+ return stopColor(first);
881
+ }
882
+ const stops = v.stops.map((s) => `${stopColor(s)} ${s.position}%`).join(", ");
883
+ if (v.type === "linear") return `linear-gradient(${v.angle}deg, ${stops})`;
884
+ const radial = v.radius && v.radius > 0 ? `circle ${v.radius}px at center` : "circle";
885
+ return `radial-gradient(${radial}, ${stops})`;
886
+ }
887
+ function aliasValueToCss(v) {
888
+ if (typeof v === "string") return v.startsWith("--") ? `var(${v})` : v;
889
+ return formatAliasGradient(v.value);
890
+ }
891
+ function aliasValuesEqual(a, b) {
892
+ if (a === void 0 || b === void 0) return a === b;
893
+ if (typeof a === "string" && typeof b === "string") return a === b;
894
+ if (typeof a !== typeof b) return false;
895
+ return JSON.stringify(a) === JSON.stringify(b);
896
+ }
403
897
  function readComponentConfig(comp, name) {
404
898
  const filePath = componentResource(comp).filePath(name);
405
899
  if (!fs2.existsSync(filePath)) return null;
@@ -409,54 +903,8 @@ ${newVars}
409
903
  return null;
410
904
  }
411
905
  }
412
- const COMPONENT_OVERRIDES_START = "/* component-aliases:start */";
413
- const COMPONENT_OVERRIDES_END = "/* component-aliases:end */";
414
906
  function syncComponentsToCss() {
415
- if (!fs2.existsSync(CSS_PATH)) return;
416
- if (!fs2.existsSync(COMPONENT_CONFIGS_DIR)) return;
417
- const lines = [];
418
- const components = listComponentNames();
419
- for (const comp of components) {
420
- const prod = componentResource(comp).getProductionName();
421
- if (prod === "default") continue;
422
- const prodCfg = readComponentConfig(comp, prod);
423
- const defaultCfg = readComponentConfig(comp, "default");
424
- if (!prodCfg || !defaultCfg) continue;
425
- const overrides = [];
426
- for (const [varName, semanticName] of Object.entries(prodCfg.aliases ?? {})) {
427
- if ((defaultCfg.aliases ?? {})[varName] !== semanticName) {
428
- overrides.push([varName, semanticName]);
429
- }
430
- }
431
- if (overrides.length === 0) continue;
432
- lines.push(` /* ${comp} (${prod}) */`);
433
- for (const [varName, semanticName] of overrides) {
434
- lines.push(` ${varName}: var(${semanticName});`);
435
- }
436
- }
437
- const block = lines.length > 0 ? `
438
-
439
- ${COMPONENT_OVERRIDES_START}
440
- :root:root {
441
- ${lines.join("\n")}
442
- }
443
- ${COMPONENT_OVERRIDES_END}
444
- ` : "";
445
- let cssContent = fs2.readFileSync(CSS_PATH, "utf-8");
446
- const startIdx = cssContent.indexOf(COMPONENT_OVERRIDES_START);
447
- const endIdx = cssContent.indexOf(COMPONENT_OVERRIDES_END);
448
- if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
449
- let stripStart = startIdx;
450
- while (stripStart > 0 && cssContent[stripStart - 1] === "\n") stripStart--;
451
- const after = cssContent.slice(endIdx + COMPONENT_OVERRIDES_END.length);
452
- cssContent = cssContent.slice(0, stripStart) + (block || "\n") + after.replace(/^\n+/, "");
453
- } else if (block) {
454
- cssContent = cssContent.replace(/\n*$/, "") + block;
455
- }
456
- fs2.writeFileSync(CSS_PATH, cssContent);
457
- console.log(
458
- `[syncComponentsToCss] Wrote ${lines.filter((l) => !l.trim().startsWith("/*")).length} alias override(s) to tokens.css`
459
- );
907
+ regenerateTokensCss();
460
908
  }
461
909
  const escapedBase = API_BASE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
462
910
  const THEMES_ROUTE = `${API_BASE}/themes`;
@@ -471,6 +919,8 @@ ${COMPONENT_OVERRIDES_END}
471
919
  const COMP_PRODUCTION_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/production$`);
472
920
  const COMP_BY_NAME_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/([a-z0-9\\-_]+)$`);
473
921
  const MANIFEST_APPLY_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/apply$`);
922
+ const MANIFEST_EXPORT_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/export$`);
923
+ const MANIFEST_IMPORT_ROUTE = `${API_BASE}/manifests/import`;
474
924
  const MANIFEST_BY_NAME_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)$`);
475
925
  async function handleListThemes(_ctx) {
476
926
  const activeFile = themesResource.getActiveName();
@@ -494,7 +944,7 @@ ${COMPONENT_OVERRIDES_END}
494
944
  jsonResponse(res, 404, { error: "Active theme not found" });
495
945
  return;
496
946
  }
497
- const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
947
+ const data = normalizeTheme(JSON.parse(fs2.readFileSync(filePath, "utf-8")));
498
948
  data._fileName = activeFile;
499
949
  jsonResponse(res, 200, data);
500
950
  }
@@ -515,7 +965,7 @@ ${COMPONENT_OVERRIDES_END}
515
965
  jsonResponse(res, 200, { fileName: prodFile, name: prodFile, cssVariables: {} });
516
966
  return;
517
967
  }
518
- const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
968
+ const data = normalizeTheme(JSON.parse(fs2.readFileSync(filePath, "utf-8")));
519
969
  jsonResponse(res, 200, {
520
970
  fileName: prodFile,
521
971
  name: data.name || prodFile,
@@ -558,7 +1008,7 @@ ${COMPONENT_OVERRIDES_END}
558
1008
  jsonResponse(res, 404, { error: "Not found" });
559
1009
  return;
560
1010
  }
561
- const data = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
1011
+ const data = normalizeTheme(JSON.parse(fs2.readFileSync(filePath, "utf-8")));
562
1012
  data._fileName = fileName;
563
1013
  jsonResponse(res, 200, data);
564
1014
  return;
@@ -878,11 +1328,15 @@ ${COMPONENT_OVERRIDES_END}
878
1328
  apply.push([comp, sanitized]);
879
1329
  }
880
1330
  themesResource.setActiveName(themeName);
881
- const themeData = JSON.parse(fs2.readFileSync(themePath, "utf-8"));
1331
+ themesResource.setProductionName(themeName);
1332
+ syncTokensToCss(themeName);
1333
+ syncFontsToCss(themeName);
1334
+ const themeData = normalizeTheme(JSON.parse(fs2.readFileSync(themePath, "utf-8")));
882
1335
  themeData._fileName = themeName;
883
1336
  for (const [comp, configFile] of apply) {
884
1337
  const r = componentResource(comp);
885
1338
  r.setActiveName(configFile);
1339
+ r.setProductionName(configFile);
886
1340
  const cfg = readComponentConfig(comp, configFile);
887
1341
  if (cfg) resolvedConfigs[comp] = { ...cfg, _fileName: configFile };
888
1342
  }
@@ -892,6 +1346,7 @@ ${COMPONENT_OVERRIDES_END}
892
1346
  const cfg = readComponentConfig(comp, activeName);
893
1347
  if (cfg) resolvedConfigs[comp] = { ...cfg, _fileName: activeName };
894
1348
  }
1349
+ syncComponentsToCss();
895
1350
  manifestsResource.setActiveName(fileName);
896
1351
  jsonResponse(res, 200, {
897
1352
  ok: true,
@@ -900,6 +1355,157 @@ ${COMPONENT_OVERRIDES_END}
900
1355
  componentConfigs: resolvedConfigs
901
1356
  });
902
1357
  }
1358
+ async function handleExportManifest({ params, res }) {
1359
+ const [fileName] = params;
1360
+ const manifestPath = manifestsResource.filePath(fileName);
1361
+ if (!fs2.existsSync(manifestPath)) {
1362
+ jsonResponse(res, 404, { error: "Manifest not found" });
1363
+ return;
1364
+ }
1365
+ const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
1366
+ const themeName = sanitizeFileName(manifest.theme || "default");
1367
+ const themePath = themesResource.filePath(themeName);
1368
+ if (!fs2.existsSync(themePath)) {
1369
+ jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
1370
+ return;
1371
+ }
1372
+ const theme = JSON.parse(fs2.readFileSync(themePath, "utf-8"));
1373
+ const knownComponents = new Set(listComponentNames());
1374
+ const componentConfigs = {};
1375
+ for (const [comp, configFile] of Object.entries(manifest.componentConfigs ?? {})) {
1376
+ if (!knownComponents.has(comp)) continue;
1377
+ const sanitized = sanitizeFileName(String(configFile) || "default");
1378
+ if (sanitized === "default") continue;
1379
+ const cfgPath = componentResource(comp).filePath(sanitized);
1380
+ if (!fs2.existsSync(cfgPath)) {
1381
+ jsonResponse(res, 422, {
1382
+ error: `Manifest references missing config: ${comp}/${sanitized}`
1383
+ });
1384
+ return;
1385
+ }
1386
+ const cfg = JSON.parse(fs2.readFileSync(cfgPath, "utf-8"));
1387
+ componentConfigs[`${comp}/${sanitized}`] = cfg;
1388
+ }
1389
+ const bundle = {
1390
+ kind: "manifest-bundle",
1391
+ schemaVersion: 1,
1392
+ liveTokensVersion: PKG_VERSION,
1393
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1394
+ manifest,
1395
+ theme,
1396
+ componentConfigs
1397
+ };
1398
+ res.statusCode = 200;
1399
+ res.setHeader("Content-Type", "application/json");
1400
+ res.setHeader(
1401
+ "Content-Disposition",
1402
+ `attachment; filename="${fileName}.bundle.json"`
1403
+ );
1404
+ res.end(JSON.stringify(bundle, null, 2));
1405
+ }
1406
+ function nextAvailableName2(resourceFilePath, baseName) {
1407
+ return nextAvailableName(
1408
+ (n) => fs2.existsSync(resourceFilePath(n)),
1409
+ sanitizeFileName(baseName)
1410
+ );
1411
+ }
1412
+ async function handleImportManifest({ req, res }) {
1413
+ let bundle;
1414
+ try {
1415
+ bundle = JSON.parse(await readBody(req));
1416
+ } catch {
1417
+ jsonResponse(res, 400, { error: "Body is not valid JSON" });
1418
+ return;
1419
+ }
1420
+ if (!bundle || bundle.kind !== "manifest-bundle") {
1421
+ jsonResponse(res, 400, {
1422
+ error: "Not a manifest bundle (kind discriminator missing or wrong)"
1423
+ });
1424
+ return;
1425
+ }
1426
+ if (bundle.schemaVersion !== 1) {
1427
+ jsonResponse(res, 400, {
1428
+ error: `Unsupported bundle schemaVersion: ${bundle.schemaVersion}`
1429
+ });
1430
+ return;
1431
+ }
1432
+ if (!bundle.manifest || !bundle.theme || !bundle.componentConfigs) {
1433
+ jsonResponse(res, 400, { error: "Bundle missing manifest / theme / componentConfigs" });
1434
+ return;
1435
+ }
1436
+ const renames = {};
1437
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1438
+ const originalThemeName = sanitizeFileName(bundle.manifest.theme || "default");
1439
+ const finalThemeName = nextAvailableName2(
1440
+ (n) => themesResource.filePath(n),
1441
+ originalThemeName
1442
+ );
1443
+ if (finalThemeName !== originalThemeName) {
1444
+ renames[`theme:${originalThemeName}`] = finalThemeName;
1445
+ }
1446
+ const themeBody = { ...bundle.theme };
1447
+ themeBody.updatedAt = now;
1448
+ if (!themeBody.createdAt) themeBody.createdAt = now;
1449
+ themesResource.ensureDir();
1450
+ fs2.writeFileSync(themesResource.filePath(finalThemeName), JSON.stringify(themeBody, null, 2));
1451
+ const knownComponents = new Set(listComponentNames());
1452
+ const componentRenames = {};
1453
+ for (const [key, cfgValue] of Object.entries(bundle.componentConfigs)) {
1454
+ const [comp, originalName] = key.split("/");
1455
+ if (!comp || !originalName) continue;
1456
+ if (!knownComponents.has(comp)) continue;
1457
+ const r = componentResource(comp);
1458
+ const finalName = nextAvailableName2(
1459
+ (n) => r.filePath(n),
1460
+ originalName
1461
+ );
1462
+ if (finalName !== originalName) {
1463
+ renames[`componentConfig:${comp}/${originalName}`] = finalName;
1464
+ }
1465
+ componentRenames[`${comp}/${originalName}`] = finalName;
1466
+ const cfgBody = { ...cfgValue };
1467
+ cfgBody.component = comp;
1468
+ cfgBody.name = finalName;
1469
+ cfgBody.updatedAt = now;
1470
+ if (!cfgBody.createdAt) cfgBody.createdAt = now;
1471
+ r.ensureDir();
1472
+ fs2.writeFileSync(r.filePath(finalName), JSON.stringify(cfgBody, null, 2));
1473
+ }
1474
+ const rewrittenManifest = {
1475
+ ...bundle.manifest,
1476
+ theme: finalThemeName,
1477
+ componentConfigs: {}
1478
+ };
1479
+ for (const [comp, configName] of Object.entries(bundle.manifest.componentConfigs ?? {})) {
1480
+ const original = String(configName);
1481
+ if (original === "default") {
1482
+ rewrittenManifest.componentConfigs[comp] = "default";
1483
+ continue;
1484
+ }
1485
+ const finalName = componentRenames[`${comp}/${original}`] ?? original;
1486
+ rewrittenManifest.componentConfigs[comp] = finalName;
1487
+ }
1488
+ rewrittenManifest.updatedAt = now;
1489
+ if (!rewrittenManifest.createdAt) rewrittenManifest.createdAt = now;
1490
+ const originalManifestName = sanitizeFileName(bundle.manifest.name || "imported");
1491
+ const finalManifestName = nextAvailableName2(
1492
+ (n) => manifestsResource.filePath(n),
1493
+ originalManifestName
1494
+ );
1495
+ if (finalManifestName !== originalManifestName) {
1496
+ renames[`manifest:${originalManifestName}`] = finalManifestName;
1497
+ }
1498
+ manifestsResource.ensureDir();
1499
+ fs2.writeFileSync(
1500
+ manifestsResource.filePath(finalManifestName),
1501
+ JSON.stringify(rewrittenManifest, null, 2)
1502
+ );
1503
+ jsonResponse(res, 200, {
1504
+ ok: true,
1505
+ manifest: finalManifestName,
1506
+ renames
1507
+ });
1508
+ }
903
1509
  function methodNotAllowed({ res }) {
904
1510
  jsonResponse(res, 405, { error: "Method not allowed" });
905
1511
  }
@@ -937,11 +1543,21 @@ ${COMPONENT_OVERRIDES_END}
937
1543
  { method: "GET", pattern: MANIFESTS_ROUTE, handler: handleListManifests },
938
1544
  { method: "GET", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleGetActiveManifest },
939
1545
  { method: "PUT", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleSetActiveManifest },
1546
+ // Manifests — exact import route runs before :name regexes
1547
+ { method: "POST", pattern: MANIFEST_IMPORT_ROUTE, handler: handleImportManifest },
1548
+ { method: "PUT", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
1549
+ { method: "GET", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
1550
+ { method: "DELETE", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
940
1551
  // Manifests — :name/apply (more specific than :name)
941
1552
  { method: "PUT", pattern: MANIFEST_APPLY_REGEX, handler: handleApplyManifest },
942
1553
  { method: "POST", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
943
1554
  { method: "GET", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
944
1555
  { method: "DELETE", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
1556
+ // Manifests — :name/export (more specific than :name)
1557
+ { method: "GET", pattern: MANIFEST_EXPORT_REGEX, handler: handleExportManifest },
1558
+ { method: "PUT", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
1559
+ { method: "POST", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
1560
+ { method: "DELETE", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
945
1561
  // Manifests — :name CRUD (broadest manifest route, runs last)
946
1562
  { method: "GET", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
947
1563
  { method: "PUT", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
@@ -961,6 +1577,7 @@ ${COMPONENT_OVERRIDES_END}
961
1577
  ensureThemesDir();
962
1578
  ensureComponentConfigsDir();
963
1579
  ensureManifestsDir();
1580
+ regenerateTokensCss();
964
1581
  server.middlewares.use(async (req, res, next) => {
965
1582
  const handled = await dispatch(req, res, routes);
966
1583
  if (!handled) next();