@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
@@ -38,8 +38,8 @@ __export(index_exports, {
38
38
  module.exports = __toCommonJS(index_exports);
39
39
 
40
40
  // vite-plugin/themeFileApi.ts
41
- var import_fs3 = __toESM(require("fs"), 1);
42
- var import_path3 = __toESM(require("path"), 1);
41
+ var import_fs4 = __toESM(require("fs"), 1);
42
+ var import_path4 = __toESM(require("path"), 1);
43
43
 
44
44
  // src/editor/core/themes/parsers/globalRootBlock.ts
45
45
  function extractGlobalRootBody(source) {
@@ -62,7 +62,7 @@ function parseColorOpacity(value) {
62
62
 
63
63
  // src/editor/core/storage/files/versionedFileResourceClient.ts
64
64
  function sanitizeFileName(name) {
65
- return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9\-_]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unnamed";
65
+ return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9\-_]/g, "").replace(/-+/g, "-").replace(/^[-_]+|-+$/g, "") || "unnamed";
66
66
  }
67
67
 
68
68
  // src/editor/core/palettes/oklch.ts
@@ -164,6 +164,25 @@ function gamutClamp(l, c, h) {
164
164
  function makeAnchor(x, y, tangentLen = 15) {
165
165
  return { x, y, inDx: -tangentLen, inDy: 0, outDx: tangentLen, outDy: 0 };
166
166
  }
167
+ var HANDLE_SPAN = 1 / 3;
168
+ function tangentSlope(prev, x, y, next) {
169
+ const hPrev = prev ? x - prev.x : 0;
170
+ const hNext = next ? next.x - x : 0;
171
+ const dPrev = hPrev > 0 ? (y - prev.y) / hPrev : null;
172
+ const dNext = hNext > 0 ? (next.y - y) / hNext : null;
173
+ if (dPrev === null) return dNext ?? 0;
174
+ if (dNext === null) return dPrev;
175
+ if (dPrev * dNext <= 0) return 0;
176
+ const wPrev = 2 * hNext + hPrev;
177
+ const wNext = hNext + 2 * hPrev;
178
+ return (wPrev + wNext) / (wPrev / dPrev + wNext / dNext);
179
+ }
180
+ function tangentAnchor(x, y, prev, next) {
181
+ const m = tangentSlope(prev, x, y, next);
182
+ const inDx = prev ? -(x - prev.x) * HANDLE_SPAN : 0;
183
+ const outDx = next ? (next.x - x) * HANDLE_SPAN : 0;
184
+ return { x, y, inDx, inDy: m * inDx, outDx, outDy: m * outDx };
185
+ }
167
186
  function evalBezier(p0x, p0y, c0x, c0y, c1x, c1y, p1x, p1y, t) {
168
187
  const u = 1 - t, u2 = u * u, u3 = u2 * u;
169
188
  const t2 = t * t, t3 = t2 * t;
@@ -330,7 +349,33 @@ function nearestPaletteStep(lightnessCurve, baseL) {
330
349
  }
331
350
  return best;
332
351
  }
333
- function setCurveAnchor(curve, x, y) {
352
+ function setCurveAnchor(curve, x, y, smooth = false) {
353
+ if (smooth && curve.length === 2) {
354
+ const [p0, p2] = curve;
355
+ const mid = { x, y, inDx: 0, inDy: 0, outDx: 0, outDy: 0 };
356
+ if (Math.abs(p0.x - x) < 0.5) {
357
+ return {
358
+ curve: [tangentAnchor(x, y, null, p2), tangentAnchor(p2.x, p2.y, mid, null)],
359
+ displacedY: p0.y,
360
+ priorEndpoints: [p0, p2]
361
+ };
362
+ }
363
+ if (Math.abs(p2.x - x) < 0.5) {
364
+ return {
365
+ curve: [tangentAnchor(p0.x, p0.y, null, mid), tangentAnchor(x, y, p0, null)],
366
+ displacedY: p2.y,
367
+ priorEndpoints: [p0, p2]
368
+ };
369
+ }
370
+ return {
371
+ curve: [
372
+ tangentAnchor(p0.x, p0.y, null, mid),
373
+ tangentAnchor(x, y, p0, p2),
374
+ tangentAnchor(p2.x, p2.y, mid, null)
375
+ ],
376
+ priorEndpoints: [p0, p2]
377
+ };
378
+ }
334
379
  const existing = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
335
380
  if (existing >= 0) {
336
381
  const displacedY = curve[existing].y;
@@ -338,18 +383,51 @@ function setCurveAnchor(curve, x, y) {
338
383
  }
339
384
  let insertAt = curve.findIndex((a) => a.x > x);
340
385
  if (insertAt < 0) insertAt = curve.length;
341
- return { curve: [...curve.slice(0, insertAt), makeAnchor(x, y, 15), ...curve.slice(insertAt)] };
386
+ const prev = curve[insertAt - 1] ?? null;
387
+ const next = curve[insertAt] ?? null;
388
+ const out = [...curve];
389
+ if (prev && next) {
390
+ const span = next.x - prev.x;
391
+ out[insertAt - 1] = scaleOutHandle(prev, (x - prev.x) / span);
392
+ out[insertAt] = scaleInHandle(next, (next.x - x) / span);
393
+ }
394
+ out.splice(insertAt, 0, fitBetween(tangentAnchor(x, y, prev, next), out[insertAt - 1] ?? null, out[insertAt] ?? null));
395
+ return { curve: out };
396
+ }
397
+ var scaleOutHandle = (a, k) => ({ ...a, outDx: a.outDx * k, outDy: a.outDy * k });
398
+ var scaleInHandle = (a, k) => ({ ...a, inDx: a.inDx * k, inDy: a.inDy * k });
399
+ function fitBetween(a, prev, next) {
400
+ let out = a;
401
+ if (prev) {
402
+ const room = a.x - (prev.x + prev.outDx);
403
+ if (room < -out.inDx) out = scaleInHandle(out, room <= 0 ? 0 : room / -out.inDx);
404
+ }
405
+ if (next) {
406
+ const room = next.x + next.inDx - a.x;
407
+ if (room < out.outDx) out = scaleOutHandle(out, room <= 0 ? 0 : room / out.outDx);
408
+ }
409
+ return out;
342
410
  }
343
- function liftCurveAnchor(curve, x, displacedY) {
411
+ function liftCurveAnchor(curve, x, displacedY, priorEndpoints) {
412
+ if (priorEndpoints) return priorEndpoints;
344
413
  const idx = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
345
414
  if (idx < 0) return curve;
346
415
  if (displacedY !== void 0) return curve.map((a, i) => i === idx ? { ...a, y: displacedY } : a);
347
416
  if (idx === 0 || idx === curve.length - 1) return curve;
348
- return curve.filter((_, i) => i !== idx);
417
+ const out = curve.filter((_, i) => i !== idx);
418
+ const prev = curve[idx - 1];
419
+ const next = curve[idx + 1];
420
+ if (prev && next) {
421
+ const span = next.x - prev.x;
422
+ out[idx - 1] = scaleOutHandle(prev, span / (curve[idx].x - prev.x));
423
+ out[idx] = scaleInHandle(next, span / (next.x - curve[idx].x));
424
+ }
425
+ return out;
349
426
  }
350
427
  function syncBaseAnchor(cfg) {
351
428
  if (cfg.anchorToBase === false) return;
352
429
  const prev = cfg.anchorPlacement;
430
+ const fresh = prev === void 0;
353
431
  let lCurve = cfg.lightnessCurve;
354
432
  let sCurve = cfg.saturationCurve;
355
433
  if (prev) {
@@ -359,11 +437,17 @@ function syncBaseAnchor(cfg) {
359
437
  }
360
438
  const step = nearestPaletteStep(lCurve, cfg.baseColor.l);
361
439
  const x = stepIndexToX(step);
362
- const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100);
363
- const s = setCurveAnchor(sCurve, x, 100);
440
+ const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100, fresh);
441
+ const s = setCurveAnchor(sCurve, x, 100, fresh);
364
442
  cfg.lightnessCurve = l.curve;
365
443
  cfg.saturationCurve = s.curve;
366
- cfg.anchorPlacement = { step, displacedL: l.displacedY, displacedS: s.displacedY };
444
+ cfg.anchorPlacement = {
445
+ step,
446
+ displacedL: l.displacedY,
447
+ displacedS: s.displacedY,
448
+ priorLightnessEndpoints: prev?.priorLightnessEndpoints ?? l.priorEndpoints,
449
+ priorSaturationEndpoints: prev?.priorSaturationEndpoints ?? s.priorEndpoints
450
+ };
367
451
  }
368
452
  function computeDerivedOklch(step, base, scaleTitle, scaleCurves, curveOffset) {
369
453
  const scale = SCALES.find((s) => s.title === scaleTitle);
@@ -540,6 +624,7 @@ function versionedFileResourceServer(opts) {
540
624
  const dir = opts.dir;
541
625
  const activePath = import_path.default.join(dir, "_active.json");
542
626
  const productionPath = import_path.default.join(dir, "_production.json");
627
+ const workingPath = import_path.default.join(dir, "_working.json");
543
628
  const defaultName = opts.defaultName ?? "default";
544
629
  const resolvedDir = import_path.default.resolve(dir);
545
630
  const resolvedPackageDir = opts.packageDir ? import_path.default.resolve(opts.packageDir) : null;
@@ -568,6 +653,16 @@ function versionedFileResourceServer(opts) {
568
653
  }
569
654
  return null;
570
655
  }
656
+ function isPackageFile(name) {
657
+ if (!resolvedPackageDir) return false;
658
+ const pkg = import_path.default.join(resolvedPackageDir, `${name}.json`);
659
+ if (!import_fs.default.existsSync(pkg)) return false;
660
+ const local = import_path.default.resolve(filePath(name));
661
+ if (local === pkg) {
662
+ return opts.packageOwnedNames ? opts.packageOwnedNames.includes(name) : true;
663
+ }
664
+ return !import_fs.default.existsSync(local);
665
+ }
571
666
  function readJson(name) {
572
667
  const resolved = existingPath(name);
573
668
  if (!resolved) return null;
@@ -612,20 +707,44 @@ function versionedFileResourceServer(opts) {
612
707
  function setProductionName(name) {
613
708
  import_fs.default.writeFileSync(productionPath, JSON.stringify({ productionFile: name }));
614
709
  }
710
+ function hasWorking() {
711
+ return import_fs.default.existsSync(workingPath);
712
+ }
713
+ function readWorking() {
714
+ if (!import_fs.default.existsSync(workingPath)) return null;
715
+ const parsed = JSON.parse(import_fs.default.readFileSync(workingPath, "utf-8"));
716
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
717
+ throw new Error(`${workingPath} does not hold a JSON object`);
718
+ }
719
+ return parsed;
720
+ }
721
+ function writeWorking(data) {
722
+ ensureDir();
723
+ import_fs.default.writeFileSync(workingPath, JSON.stringify(data, null, 2));
724
+ }
725
+ function clearWorking() {
726
+ if (import_fs.default.existsSync(workingPath)) import_fs.default.unlinkSync(workingPath);
727
+ }
615
728
  return {
616
729
  dir,
617
730
  activePath,
618
731
  productionPath,
732
+ workingPath,
619
733
  ensureDir,
620
734
  ensureMeta,
621
735
  filePath,
622
736
  existingPath,
737
+ isPackageFile,
623
738
  readJson,
624
739
  listNames,
625
740
  getActiveName,
626
741
  getProductionName,
627
742
  setActiveName,
628
- setProductionName
743
+ setProductionName,
744
+ hasWorking,
745
+ readWorking,
746
+ writeWorking,
747
+ clearWorking
629
748
  };
630
749
  }
631
750
 
@@ -657,6 +776,78 @@ async function dispatch(req, res, routes) {
657
776
  return false;
658
777
  }
659
778
 
779
+ // vite-plugin/themes/normalizeTheme.ts
780
+ var THEME_SCHEMA_VERSION = 3;
781
+ function asObject(value) {
782
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
783
+ }
784
+ function isColorsAndTypeShaped(raw) {
785
+ const obj = asObject(raw);
786
+ if (!obj) return false;
787
+ if ("theme" in obj || "colorsAndType" in obj) return false;
788
+ return "cssVariables" in obj || "editorConfigs" in obj;
789
+ }
790
+ function stripFileMarker(value) {
791
+ if (!("_fileName" in value)) return value;
792
+ const { _fileName, ...rest } = value;
793
+ return rest;
794
+ }
795
+ function asString(value, fallback) {
796
+ return typeof value === "string" && value ? value : fallback;
797
+ }
798
+ function migrateEmbeddedKey(src) {
799
+ const { theme, ...rest } = src;
800
+ return { ...rest, colorsAndType: theme };
801
+ }
802
+ function normalizeTheme(raw, resolvers) {
803
+ const input = asObject(raw) ?? {};
804
+ const migrated = input.schemaVersion !== THEME_SCHEMA_VERSION;
805
+ const src = migrated ? migrateEmbeddedKey(input) : input;
806
+ const dropped = [];
807
+ const now = (/* @__PURE__ */ new Date()).toISOString();
808
+ const embeddedColorsAndType = asObject(src.colorsAndType);
809
+ let colorsAndType = null;
810
+ if (embeddedColorsAndType) {
811
+ colorsAndType = stripFileMarker(embeddedColorsAndType);
812
+ } else {
813
+ const colorsAndTypeName = asString(src.colorsAndType, "default");
814
+ colorsAndType = asObject(resolvers.readColorsAndType(colorsAndTypeName));
815
+ if (!colorsAndType) {
816
+ dropped.push(`colors-and-type:${colorsAndTypeName}`);
817
+ if (colorsAndTypeName !== "default") colorsAndType = asObject(resolvers.readColorsAndType("default"));
818
+ }
819
+ if (colorsAndType) colorsAndType = resolvers.normalizeColorsAndType(stripFileMarker(colorsAndType));
820
+ }
821
+ const componentConfigs = {};
822
+ for (const [comp, value] of Object.entries(asObject(src.componentConfigs) ?? {})) {
823
+ const embedded = asObject(value);
824
+ if (embedded) {
825
+ componentConfigs[comp] = stripFileMarker(embedded);
826
+ continue;
827
+ }
828
+ const configName = asString(value, "default");
829
+ if (configName === "default") continue;
830
+ const resolved = asObject(resolvers.readComponentConfig(comp, configName));
831
+ if (!resolved) {
832
+ dropped.push(`${comp}/${configName}`);
833
+ continue;
834
+ }
835
+ componentConfigs[comp] = stripFileMarker(resolved);
836
+ }
837
+ return {
838
+ theme: {
839
+ name: asString(src.name, "Untitled"),
840
+ createdAt: asString(src.createdAt, now),
841
+ updatedAt: asString(src.updatedAt, now),
842
+ schemaVersion: THEME_SCHEMA_VERSION,
843
+ colorsAndType,
844
+ componentConfigs
845
+ },
846
+ dropped,
847
+ migrated
848
+ };
849
+ }
850
+
660
851
  // vite-plugin/files/nameAllocator.ts
661
852
  function nextAvailableName(exists, baseName, maxAttempts = 1e3) {
662
853
  if (!exists(baseName)) return baseName;
@@ -673,8 +864,9 @@ var import_path2 = __toESM(require("path"), 1);
673
864
  var DEFAULT_DATA_DIR = "src/live-tokens/data";
674
865
  var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
675
866
  "dataDir",
676
- "themesDir",
867
+ "colorsAndTypeDir",
677
868
  "componentConfigsDir",
869
+ "themesDir",
678
870
  "manifestsDir",
679
871
  "tokensCssPath"
680
872
  ]);
@@ -707,12 +899,69 @@ function resolveDataDirs(opts = {}) {
707
899
  const sub = (name) => import_path2.default.resolve(dataDir, name);
708
900
  return {
709
901
  dataDir,
710
- themesDir: opts.themesDir ? import_path2.default.resolve(opts.themesDir) : fileConfig.themesDir ? import_path2.default.resolve(fileConfig.themesDir) : sub("themes"),
902
+ colorsAndTypeDir: opts.colorsAndTypeDir ? import_path2.default.resolve(opts.colorsAndTypeDir) : fileConfig.colorsAndTypeDir ? import_path2.default.resolve(fileConfig.colorsAndTypeDir) : sub("colors-and-type"),
711
903
  componentConfigsDir: opts.componentConfigsDir ? import_path2.default.resolve(opts.componentConfigsDir) : fileConfig.componentConfigsDir ? import_path2.default.resolve(fileConfig.componentConfigsDir) : sub("component-configs"),
712
- manifestsDir: opts.manifestsDir ? import_path2.default.resolve(opts.manifestsDir) : fileConfig.manifestsDir ? import_path2.default.resolve(fileConfig.manifestsDir) : sub("manifests")
904
+ themesDir: opts.themesDir ? import_path2.default.resolve(opts.themesDir) : fileConfig.themesDir ? import_path2.default.resolve(fileConfig.themesDir) : sub("themes"),
905
+ legacyManifestsDir: fileConfig.manifestsDir ? import_path2.default.resolve(fileConfig.manifestsDir) : void 0
713
906
  };
714
907
  }
715
908
 
909
+ // vite-plugin/files/legacyLayout.ts
910
+ var import_fs3 = __toESM(require("fs"), 1);
911
+ var import_path3 = __toESM(require("path"), 1);
912
+ function jsonFiles(dir) {
913
+ if (!import_fs3.default.existsSync(dir)) return [];
914
+ try {
915
+ return import_fs3.default.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => import_path3.default.join(dir, f));
916
+ } catch {
917
+ return [];
918
+ }
919
+ }
920
+ function isDirectory(dir) {
921
+ try {
922
+ return import_fs3.default.statSync(dir).isDirectory();
923
+ } catch {
924
+ return false;
925
+ }
926
+ }
927
+ function parseJsonFile(file) {
928
+ try {
929
+ return JSON.parse(import_fs3.default.readFileSync(file, "utf-8"));
930
+ } catch {
931
+ return null;
932
+ }
933
+ }
934
+ function holdsATheme(themesDir) {
935
+ return jsonFiles(themesDir).some((file) => {
936
+ if (import_path3.default.basename(file).startsWith("_")) return false;
937
+ const raw = parseJsonFile(file);
938
+ if (!raw || typeof raw !== "object") return false;
939
+ return "colorsAndType" in raw || "theme" in raw;
940
+ });
941
+ }
942
+ function detectLegacyLayout(input) {
943
+ const manifestsDir = input.configuredManifestsDir ?? import_path3.default.join(input.dataDir, "manifests");
944
+ const themesHoldAThemeFile = holdsATheme(input.themesDir);
945
+ if (isDirectory(manifestsDir) && !themesHoldAThemeFile) {
946
+ return { manifestsDir, evidence: `${manifestsDir} is the pre-0.48 whole-look directory` };
947
+ }
948
+ if (jsonFiles(input.colorsAndTypeDir).length > 0) return null;
949
+ for (const file of jsonFiles(input.themesDir)) {
950
+ if (import_path3.default.basename(file).startsWith("_")) continue;
951
+ if (isColorsAndTypeShaped(parseJsonFile(file))) {
952
+ return { manifestsDir, evidence: `${file} is a colors-and-type file, not a theme` };
953
+ }
954
+ }
955
+ const pointer = ["_active.json", "_production.json"].map((name) => import_path3.default.join(input.themesDir, name)).find((p) => import_fs3.default.existsSync(p));
956
+ if (pointer && !themesHoldAThemeFile && !import_fs3.default.existsSync(input.colorsAndTypeDir)) {
957
+ return {
958
+ manifestsDir,
959
+ evidence: `${pointer} points at colors and type, and there is no ${import_path3.default.basename(input.colorsAndTypeDir)} directory`
960
+ };
961
+ }
962
+ return null;
963
+ }
964
+
716
965
  // vite-plugin/tokensCssMigrations/cssTokenOps.ts
717
966
  var DECL_RE = /(^|[\s;{])(--[a-z0-9-]+)\s*:/gi;
718
967
  var REF_RE = /var\(\s*(--[a-z0-9-]+)/gi;
@@ -1113,14 +1362,14 @@ var import_node_url = require("url");
1113
1362
  var import_meta = {};
1114
1363
  var PKG_VERSION = (() => {
1115
1364
  try {
1116
- let dir = import_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1365
+ let dir = import_path4.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1117
1366
  for (let i = 0; i < 4; i++) {
1118
- const p = import_path3.default.join(dir, "package.json");
1119
- if (import_fs3.default.existsSync(p)) {
1120
- const json = JSON.parse(import_fs3.default.readFileSync(p, "utf-8"));
1367
+ const p = import_path4.default.join(dir, "package.json");
1368
+ if (import_fs4.default.existsSync(p)) {
1369
+ const json = JSON.parse(import_fs4.default.readFileSync(p, "utf-8"));
1121
1370
  if (json?.name === "@motion-proto/live-tokens") return json.version ?? "";
1122
1371
  }
1123
- const up = import_path3.default.dirname(dir);
1372
+ const up = import_path4.default.dirname(dir);
1124
1373
  if (up === dir) break;
1125
1374
  dir = up;
1126
1375
  }
@@ -1128,65 +1377,79 @@ var PKG_VERSION = (() => {
1128
1377
  }
1129
1378
  return "";
1130
1379
  })();
1380
+ var BUNDLE_KIND_BY_VERSION = {
1381
+ 1: "manifest-bundle",
1382
+ [THEME_SCHEMA_VERSION]: "theme-bundle"
1383
+ };
1131
1384
  function themeFileApi(opts) {
1132
1385
  const dataDirs = resolveDataDirs({
1133
1386
  dataDir: opts.dataDir,
1134
- themesDir: opts.themesDir,
1387
+ colorsAndTypeDir: opts.colorsAndTypeDir,
1135
1388
  componentConfigsDir: opts.componentConfigsDir,
1136
- manifestsDir: opts.manifestsDir
1389
+ themesDir: opts.themesDir
1137
1390
  });
1138
- const THEMES_DIR = dataDirs.themesDir;
1391
+ const COLORS_AND_TYPE_DIR = dataDirs.colorsAndTypeDir;
1139
1392
  const COMPONENT_CONFIGS_DIR = dataDirs.componentConfigsDir;
1140
- const MANIFESTS_DIR = dataDirs.manifestsDir;
1141
- const CSS_PATH = import_path3.default.resolve(opts.tokensCssPath);
1142
- const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? import_path3.default.resolve(opts.tokensGeneratedCssPath) : import_path3.default.join(dataDirs.dataDir, "tokens.generated.css");
1143
- const FONTS_CSS_PATH = opts.fontsCssPath ? import_path3.default.resolve(opts.fontsCssPath) : import_path3.default.join(import_path3.default.dirname(CSS_PATH), "fonts.css");
1393
+ const THEMES_DIR = dataDirs.themesDir;
1394
+ const CSS_PATH = import_path4.default.resolve(opts.tokensCssPath);
1395
+ const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? import_path4.default.resolve(opts.tokensGeneratedCssPath) : import_path4.default.join(dataDirs.dataDir, "tokens.generated.css");
1396
+ const FONTS_CSS_PATH = opts.fontsCssPath ? import_path4.default.resolve(opts.fontsCssPath) : import_path4.default.join(import_path4.default.dirname(CSS_PATH), "fonts.css");
1144
1397
  const API_BASE = opts.apiBase ?? "/api/live-tokens";
1145
- const consumerComponentDirs = opts.componentsSrcDir ? [import_path3.default.resolve(opts.componentsSrcDir)] : [import_path3.default.resolve("src/components"), import_path3.default.resolve("src/system/components")];
1146
- const packageComponentsDir = import_path3.default.resolve(
1147
- import_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
1398
+ const consumerComponentDirs = opts.componentsSrcDir ? [import_path4.default.resolve(opts.componentsSrcDir)] : [import_path4.default.resolve("src/components"), import_path4.default.resolve("src/system/components")];
1399
+ const packageComponentsDir = import_path4.default.resolve(
1400
+ import_path4.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
1148
1401
  "..",
1149
1402
  "src",
1150
1403
  "system",
1151
1404
  "components"
1152
1405
  );
1153
- const packageDataDir = import_path3.default.resolve(
1154
- import_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
1406
+ const packageDataDir = import_path4.default.resolve(
1407
+ import_path4.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
1155
1408
  "..",
1156
1409
  "src",
1157
1410
  "live-tokens",
1158
1411
  "data"
1159
1412
  );
1160
- const packageThemesDir = import_path3.default.join(packageDataDir, "themes");
1161
- const packageManifestsDir = import_path3.default.join(packageDataDir, "manifests");
1162
- const packageComponentConfigsDir = import_path3.default.join(packageDataDir, "component-configs");
1413
+ const packageColorsAndTypeDir = import_path4.default.join(packageDataDir, "colors-and-type");
1414
+ const packageThemesDir = import_path4.default.join(packageDataDir, "themes");
1415
+ const packageComponentConfigsDir = import_path4.default.join(packageDataDir, "component-configs");
1416
+ function shippedColorsAndTypeNames(dataDir) {
1417
+ try {
1418
+ const pkgRoot = import_path4.default.resolve(dataDir, "..", "..", "..");
1419
+ const pkg = JSON.parse(import_fs4.default.readFileSync(import_path4.default.join(pkgRoot, "package.json"), "utf-8"));
1420
+ const files = Array.isArray(pkg.files) ? pkg.files : [];
1421
+ return files.filter((f) => /^src\/live-tokens\/data\/colors-and-type\/[^/]+\.json$/.test(f)).map((f) => import_path4.default.basename(f, ".json"));
1422
+ } catch {
1423
+ return [];
1424
+ }
1425
+ }
1163
1426
  const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
1164
- if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs3.default.existsSync(packageComponentsDir)) {
1427
+ if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs4.default.existsSync(packageComponentsDir)) {
1165
1428
  COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
1166
1429
  }
1167
- const themesResource = versionedFileResourceServer({
1168
- dir: THEMES_DIR,
1169
- packageDir: packageThemesDir
1430
+ const colorsAndTypeResource = versionedFileResourceServer({
1431
+ dir: COLORS_AND_TYPE_DIR,
1432
+ packageDir: packageColorsAndTypeDir,
1433
+ packageOwnedNames: shippedColorsAndTypeNames(packageDataDir)
1170
1434
  });
1171
1435
  const componentResourceCache = /* @__PURE__ */ new Map();
1172
1436
  function componentResource(comp) {
1173
1437
  let r = componentResourceCache.get(comp);
1174
1438
  if (!r) {
1175
1439
  r = versionedFileResourceServer({
1176
- dir: import_path3.default.join(COMPONENT_CONFIGS_DIR, comp),
1177
- packageDir: import_path3.default.join(packageComponentConfigsDir, comp)
1440
+ dir: import_path4.default.join(COMPONENT_CONFIGS_DIR, comp),
1441
+ packageDir: import_path4.default.join(packageComponentConfigsDir, comp)
1178
1442
  });
1179
1443
  componentResourceCache.set(comp, r);
1180
1444
  }
1181
1445
  return r;
1182
1446
  }
1183
- const manifestsResource = versionedFileResourceServer({
1184
- dir: MANIFESTS_DIR,
1185
- packageDir: packageManifestsDir
1447
+ const themesResource = versionedFileResourceServer({
1448
+ dir: THEMES_DIR,
1449
+ packageDir: packageThemesDir
1186
1450
  });
1187
- function ensureThemesDir() {
1188
- themesResource.ensureDir();
1189
- themesResource.ensureMeta();
1451
+ function ensureColorsAndTypeDir() {
1452
+ colorsAndTypeResource.ensureDir();
1190
1453
  }
1191
1454
  function readBody(req) {
1192
1455
  return new Promise((resolve, reject) => {
@@ -1201,25 +1464,33 @@ function themeFileApi(opts) {
1201
1464
  res.setHeader("Content-Type", "application/json");
1202
1465
  res.end(JSON.stringify(data));
1203
1466
  }
1204
- function normalizeTheme(theme) {
1205
- if (!theme || typeof theme !== "object") return theme;
1206
- const palettes = theme.editorConfigs ?? {};
1207
- const cssVars = theme.cssVariables ?? {};
1467
+ function rejectReservedName(res, name) {
1468
+ if (!name.startsWith("_")) return false;
1469
+ jsonResponse(res, 400, {
1470
+ error: 'Names starting with "_" are reserved',
1471
+ code: "RESERVED_NAME"
1472
+ });
1473
+ return true;
1474
+ }
1475
+ function normalizeColorsAndType(colorsAndType) {
1476
+ if (!colorsAndType || typeof colorsAndType !== "object") return colorsAndType;
1477
+ const palettes = colorsAndType.editorConfigs ?? {};
1478
+ const cssVars = colorsAndType.cssVariables ?? {};
1208
1479
  const { palettes: nextPalettes, consumed } = reconcilePalettesFromCssVars(palettes, cssVars);
1209
1480
  const nextCssVars = {};
1210
1481
  for (const [k, v] of Object.entries(cssVars)) {
1211
1482
  if (!consumed.has(k)) nextCssVars[k] = v;
1212
1483
  }
1213
- return { ...theme, editorConfigs: nextPalettes, cssVariables: nextCssVars };
1484
+ return { ...colorsAndType, editorConfigs: nextPalettes, cssVariables: nextCssVars };
1214
1485
  }
1215
1486
  const SYSTEM_CASCADES_SSR = {
1216
1487
  "system-ui-sans": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
1217
1488
  "system-ui-serif": 'Georgia, "Times New Roman", serif',
1218
1489
  "system-ui-mono": 'ui-monospace, "SF Mono", Menlo, Consolas, monospace'
1219
1490
  };
1220
- function resolveFontStacks(themeData) {
1221
- const stacks = themeData.fontStacks;
1222
- const sources = themeData.fontSources;
1491
+ function resolveFontStacks(colorsAndTypeData) {
1492
+ const stacks = colorsAndTypeData.fontStacks;
1493
+ const sources = colorsAndTypeData.fontSources;
1223
1494
  if (!stacks || stacks.length === 0) return {};
1224
1495
  const familyById = /* @__PURE__ */ new Map();
1225
1496
  for (const src of sources ?? []) {
@@ -1245,21 +1516,28 @@ function themeFileApi(opts) {
1245
1516
  }
1246
1517
  function regenerateTokensCss() {
1247
1518
  const lines = [];
1248
- lines.push("/* Generated by themeFileApi from the production theme and component configs. Do not edit. */");
1519
+ lines.push("/* Generated by themeFileApi from the production theme. Do not edit. */");
1249
1520
  lines.push("/* tokens.css holds developer-authored defaults; this file holds editor overrides. */");
1250
1521
  lines.push("");
1251
1522
  const productionThemeName = themesResource.getProductionName();
1252
- const themeData = themesResource.readJson(productionThemeName);
1253
- let themeVarCount = 0;
1254
- if (themeData) {
1255
- const cssVars = { ...themeData.cssVariables || {} };
1256
- Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(themeData.editorConfigs ?? {})));
1257
- const resolvedFontVars = resolveFontStacks(themeData);
1523
+ const productionTheme = readTheme(productionThemeName)?.theme ?? null;
1524
+ if (themesResource.existingPath(productionThemeName) !== null && !productionTheme?.colorsAndType) {
1525
+ const tail = `${import_path4.default.basename(GENERATED_CSS_PATH)} was left as it was.`;
1526
+ throw new Error(
1527
+ 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.`
1528
+ );
1529
+ }
1530
+ const colorsAndTypeData = productionTheme?.colorsAndType;
1531
+ let colorsAndTypeVarCount = 0;
1532
+ if (colorsAndTypeData) {
1533
+ const cssVars = { ...colorsAndTypeData.cssVariables || {} };
1534
+ Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(colorsAndTypeData.editorConfigs ?? {})));
1535
+ const resolvedFontVars = resolveFontStacks(colorsAndTypeData);
1258
1536
  for (const [name, value] of Object.entries(resolvedFontVars)) {
1259
1537
  cssVars[name] = value;
1260
1538
  }
1261
- themeVarCount = Object.keys(cssVars).length;
1262
- if (themeVarCount > 0) {
1539
+ colorsAndTypeVarCount = Object.keys(cssVars).length;
1540
+ if (colorsAndTypeVarCount > 0) {
1263
1541
  lines.push(`/* Production theme: ${productionThemeName} */`);
1264
1542
  lines.push(":root:root {");
1265
1543
  for (const [name, value] of Object.entries(cssVars)) {
@@ -1270,12 +1548,10 @@ function themeFileApi(opts) {
1270
1548
  }
1271
1549
  }
1272
1550
  let componentOverrideCount = 0;
1273
- if (import_fs3.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1551
+ if (import_fs4.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1274
1552
  const blocks = [];
1275
1553
  for (const comp of listComponentNames()) {
1276
- const prod = componentResource(comp).getProductionName();
1277
- if (prod === "default") continue;
1278
- const prodCfg = readComponentConfig(comp, prod);
1554
+ const prodCfg = productionTheme?.componentConfigs[comp];
1279
1555
  const defaultCfg = readComponentConfig(comp, "default");
1280
1556
  if (!prodCfg || !defaultCfg) continue;
1281
1557
  const overrides = [];
@@ -1286,7 +1562,7 @@ function themeFileApi(opts) {
1286
1562
  }
1287
1563
  }
1288
1564
  if (overrides.length === 0) continue;
1289
- const block = [` /* ${comp} (${prod}) */`];
1565
+ const block = [` /* ${comp} */`];
1290
1566
  for (const [varName, semanticValue] of overrides) {
1291
1567
  block.push(` ${varName}: ${aliasValueToCss(semanticValue)};`);
1292
1568
  }
@@ -1304,24 +1580,20 @@ function themeFileApi(opts) {
1304
1580
  lines.push("");
1305
1581
  }
1306
1582
  }
1307
- if (!import_fs3.default.existsSync(import_path3.default.dirname(GENERATED_CSS_PATH))) {
1308
- import_fs3.default.mkdirSync(import_path3.default.dirname(GENERATED_CSS_PATH), { recursive: true });
1583
+ if (!import_fs4.default.existsSync(import_path4.default.dirname(GENERATED_CSS_PATH))) {
1584
+ import_fs4.default.mkdirSync(import_path4.default.dirname(GENERATED_CSS_PATH), { recursive: true });
1309
1585
  }
1310
- import_fs3.default.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
1586
+ import_fs4.default.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
1311
1587
  console.log(
1312
- `[regenerateTokensCss] Wrote ${import_path3.default.basename(GENERATED_CSS_PATH)} (${themeVarCount} theme vars, ${componentOverrideCount} component overrides)`
1588
+ `[regenerateTokensCss] Wrote ${import_path4.default.basename(GENERATED_CSS_PATH)} (${colorsAndTypeVarCount} theme vars, ${componentOverrideCount} component overrides)`
1313
1589
  );
1314
1590
  }
1315
- function syncTokensToCss(_fileName) {
1316
- regenerateTokensCss();
1317
- }
1318
- function syncFontsToCss(fileName) {
1319
- const themeData = themesResource.readJson(fileName);
1320
- if (!themeData) return;
1321
- const sources = themeData.fontSources;
1591
+ function syncFontsToCss(colorsAndTypeData) {
1592
+ if (!colorsAndTypeData) return;
1593
+ const sources = colorsAndTypeData.fontSources;
1322
1594
  if (!sources) return;
1323
1595
  const lines = [];
1324
- lines.push("/* Generated from the production theme by syncFontsToCss. Do not edit. */");
1596
+ lines.push("/* Generated from the production colors and type by syncFontsToCss. Do not edit. */");
1325
1597
  lines.push("/* Both fonts.css and fonts/ are in dist/, so relative paths work at runtime. */");
1326
1598
  lines.push("");
1327
1599
  const urlSources = sources.filter((s) => s.kind !== "font-face" && s.url);
@@ -1341,11 +1613,11 @@ function themeFileApi(opts) {
1341
1613
  lines.push("");
1342
1614
  }
1343
1615
  const content = lines.join("\n");
1344
- if (!import_fs3.default.existsSync(import_path3.default.dirname(FONTS_CSS_PATH))) {
1345
- import_fs3.default.mkdirSync(import_path3.default.dirname(FONTS_CSS_PATH), { recursive: true });
1616
+ if (!import_fs4.default.existsSync(import_path4.default.dirname(FONTS_CSS_PATH))) {
1617
+ import_fs4.default.mkdirSync(import_path4.default.dirname(FONTS_CSS_PATH), { recursive: true });
1346
1618
  }
1347
- import_fs3.default.writeFileSync(FONTS_CSS_PATH, content);
1348
- console.log(`[syncFontsToCss] Wrote ${sources.length} source(s) into ${import_path3.default.basename(FONTS_CSS_PATH)}`);
1619
+ import_fs4.default.writeFileSync(FONTS_CSS_PATH, content);
1620
+ console.log(`[syncFontsToCss] Wrote ${sources.length} source(s) into ${import_path4.default.basename(FONTS_CSS_PATH)}`);
1349
1621
  }
1350
1622
  function extractAliasDeclarations(body) {
1351
1623
  const aliases = {};
@@ -1360,11 +1632,11 @@ function themeFileApi(opts) {
1360
1632
  return aliases;
1361
1633
  }
1362
1634
  function componentNameFromFile(filePath) {
1363
- return import_path3.default.basename(filePath, ".svelte").toLowerCase();
1635
+ return import_path4.default.basename(filePath, ".svelte").toLowerCase();
1364
1636
  }
1365
1637
  function isThemeAwareComponent(filePath) {
1366
1638
  try {
1367
- const source = import_fs3.default.readFileSync(filePath, "utf-8");
1639
+ const source = import_fs4.default.readFileSync(filePath, "utf-8");
1368
1640
  return /:global\(:root\)\s*\{/.test(source);
1369
1641
  } catch {
1370
1642
  return false;
@@ -1373,10 +1645,10 @@ function themeFileApi(opts) {
1373
1645
  function listComponentSourcePaths() {
1374
1646
  const byName = /* @__PURE__ */ new Map();
1375
1647
  for (const dir of COMPONENTS_SCAN_DIRS) {
1376
- if (!import_fs3.default.existsSync(dir)) continue;
1377
- for (const f of import_fs3.default.readdirSync(dir)) {
1648
+ if (!import_fs4.default.existsSync(dir)) continue;
1649
+ for (const f of import_fs4.default.readdirSync(dir)) {
1378
1650
  if (!f.endsWith(".svelte")) continue;
1379
- const full = import_path3.default.join(dir, f);
1651
+ const full = import_path4.default.join(dir, f);
1380
1652
  if (!isThemeAwareComponent(full)) continue;
1381
1653
  const name = componentNameFromFile(f);
1382
1654
  if (!byName.has(name)) byName.set(name, full);
@@ -1390,18 +1662,18 @@ function themeFileApi(opts) {
1390
1662
  function warnOnTokenDrift(log) {
1391
1663
  let tokensCss = "";
1392
1664
  try {
1393
- tokensCss = import_fs3.default.readFileSync(CSS_PATH, "utf-8");
1665
+ tokensCss = import_fs4.default.readFileSync(CSS_PATH, "utf-8");
1394
1666
  } catch {
1395
1667
  return;
1396
1668
  }
1397
1669
  let generatedCss = "";
1398
1670
  try {
1399
- generatedCss = import_fs3.default.readFileSync(GENERATED_CSS_PATH, "utf-8");
1671
+ generatedCss = import_fs4.default.readFileSync(GENERATED_CSS_PATH, "utf-8");
1400
1672
  } catch {
1401
1673
  }
1402
1674
  const componentSources = listComponentSourcePaths().map((p) => ({
1403
1675
  name: componentNameFromFile(p),
1404
- source: import_fs3.default.readFileSync(p, "utf-8")
1676
+ source: import_fs4.default.readFileSync(p, "utf-8")
1405
1677
  }));
1406
1678
  const missing = validateTokensCss({ tokensCss, generatedCss, componentSources });
1407
1679
  if (missing.length === 0) return;
@@ -1409,7 +1681,7 @@ function themeFileApi(opts) {
1409
1681
  (m) => ` ${m.token} (referenced by ${m.referencedBy.join(", ")})`
1410
1682
  );
1411
1683
  log(
1412
- `[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${import_path3.default.relative(process.cwd(), CSS_PATH)}:
1684
+ `[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${import_path4.default.relative(process.cwd(), CSS_PATH)}:
1413
1685
  ${lines.join("\n")}
1414
1686
  These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
1415
1687
  );
@@ -1417,37 +1689,37 @@ ${lines.join("\n")}
1417
1689
  function autoMigrateAdditive(log) {
1418
1690
  let before = "";
1419
1691
  try {
1420
- before = import_fs3.default.readFileSync(CSS_PATH, "utf-8");
1692
+ before = import_fs4.default.readFileSync(CSS_PATH, "utf-8");
1421
1693
  } catch {
1422
1694
  return;
1423
1695
  }
1424
1696
  const { css, applied, changed } = runAdditiveTokensCssMigrations(before);
1425
1697
  if (!changed) return;
1426
- import_fs3.default.writeFileSync(CSS_PATH, css);
1698
+ import_fs4.default.writeFileSync(CSS_PATH, css);
1427
1699
  log(
1428
- `[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${import_path3.default.relative(process.cwd(), CSS_PATH)}: ${applied.join(", ")}. Review the diff in git.`
1700
+ `[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${import_path4.default.relative(process.cwd(), CSS_PATH)}: ${applied.join(", ")}. Review the diff in git.`
1429
1701
  );
1430
1702
  }
1431
1703
  function generateDefaultConfig(comp, sourcePath) {
1432
- if (!import_fs3.default.existsSync(sourcePath)) return;
1704
+ if (!import_fs4.default.existsSync(sourcePath)) return;
1433
1705
  const r = componentResource(comp);
1434
1706
  r.ensureDir();
1435
1707
  const defaultPath = r.filePath("default");
1436
- const sourceStat = import_fs3.default.statSync(sourcePath);
1437
- if (import_fs3.default.existsSync(defaultPath)) {
1438
- const defaultStat = import_fs3.default.statSync(defaultPath);
1708
+ const sourceStat = import_fs4.default.statSync(sourcePath);
1709
+ if (import_fs4.default.existsSync(defaultPath)) {
1710
+ const defaultStat = import_fs4.default.statSync(defaultPath);
1439
1711
  if (defaultStat.mtimeMs >= sourceStat.mtimeMs) return;
1440
1712
  }
1441
- const source = import_fs3.default.readFileSync(sourcePath, "utf-8");
1713
+ const source = import_fs4.default.readFileSync(sourcePath, "utf-8");
1442
1714
  const body = extractGlobalRootBody(source);
1443
1715
  const aliases = extractAliasDeclarations(body);
1444
1716
  const now = (/* @__PURE__ */ new Date()).toISOString();
1445
1717
  let createdAt = now;
1446
1718
  let existingUpdatedAt;
1447
1719
  let existingAliases;
1448
- if (import_fs3.default.existsSync(defaultPath)) {
1720
+ if (import_fs4.default.existsSync(defaultPath)) {
1449
1721
  try {
1450
- const existing = JSON.parse(import_fs3.default.readFileSync(defaultPath, "utf-8"));
1722
+ const existing = JSON.parse(import_fs4.default.readFileSync(defaultPath, "utf-8"));
1451
1723
  if (existing.createdAt) createdAt = existing.createdAt;
1452
1724
  if (typeof existing.updatedAt === "string") existingUpdatedAt = existing.updatedAt;
1453
1725
  if (existing.aliases && typeof existing.aliases === "object") {
@@ -1473,57 +1745,191 @@ ${lines.join("\n")}
1473
1745
  };
1474
1746
  if (aliasesUnchanged && existingUpdatedAt) {
1475
1747
  const t = /* @__PURE__ */ new Date();
1476
- import_fs3.default.utimesSync(defaultPath, t, t);
1748
+ import_fs4.default.utimesSync(defaultPath, t, t);
1477
1749
  return;
1478
1750
  }
1479
- import_fs3.default.writeFileSync(defaultPath, JSON.stringify(defaultConfig, null, 2));
1751
+ import_fs4.default.writeFileSync(defaultPath, JSON.stringify(defaultConfig, null, 2));
1480
1752
  }
1481
1753
  function ensureComponentConfigsDir() {
1482
- if (!import_fs3.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1483
- import_fs3.default.mkdirSync(COMPONENT_CONFIGS_DIR, { recursive: true });
1754
+ if (!import_fs4.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1755
+ import_fs4.default.mkdirSync(COMPONENT_CONFIGS_DIR, { recursive: true });
1484
1756
  }
1485
1757
  for (const sourcePath of listComponentSourcePaths()) {
1486
1758
  const comp = componentNameFromFile(sourcePath);
1487
1759
  const r = componentResource(comp);
1488
1760
  r.ensureDir();
1489
1761
  generateDefaultConfig(comp, sourcePath);
1490
- r.ensureMeta();
1491
1762
  }
1492
1763
  }
1493
- function ensureManifestsDir() {
1494
- manifestsResource.ensureDir();
1495
- if (!import_fs3.default.existsSync(manifestsResource.activePath)) {
1496
- import_fs3.default.writeFileSync(
1497
- manifestsResource.activePath,
1498
- JSON.stringify({ activeFile: "default" })
1499
- );
1500
- } else {
1501
- const activeName = manifestsResource.getActiveName();
1502
- if (manifestsResource.existingPath(activeName) === null) {
1503
- manifestsResource.setActiveName("default");
1764
+ function listLegacyPointers() {
1765
+ const legacy = [];
1766
+ const check = (dir) => {
1767
+ for (const name of ["_active.json", "_production.json"]) {
1768
+ const p = import_path4.default.join(dir, name);
1769
+ if (import_fs4.default.existsSync(p)) legacy.push(import_path4.default.relative(process.cwd(), p));
1770
+ }
1771
+ };
1772
+ check(COLORS_AND_TYPE_DIR);
1773
+ if (import_fs4.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1774
+ for (const entry of import_fs4.default.readdirSync(COMPONENT_CONFIGS_DIR, { withFileTypes: true })) {
1775
+ if (entry.isDirectory()) check(import_path4.default.join(COMPONENT_CONFIGS_DIR, entry.name));
1504
1776
  }
1505
1777
  }
1778
+ return legacy;
1779
+ }
1780
+ function warnOnLegacyPointers(legacy, bakeHeld, warn) {
1781
+ if (legacy.length === 0) return;
1782
+ warn(
1783
+ `[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 ${import_path4.default.basename(GENERATED_CSS_PATH)} is left exactly as it is rather than rebuilt from the default.` : "")
1784
+ );
1785
+ }
1786
+ function warnOnLegacyLayout(evidence, warn) {
1787
+ warn(
1788
+ `[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 ${import_path4.default.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.`
1789
+ );
1506
1790
  }
1507
- function patchActiveManifest(field, comp, fileName) {
1508
- const activeFile = manifestsResource.getActiveName();
1509
- if (activeFile === "default") return false;
1510
- const manifestPath = manifestsResource.filePath(activeFile);
1511
- if (!import_fs3.default.existsSync(manifestPath)) return false;
1512
- let manifest;
1791
+ const diskThemeResolvers = {
1792
+ readColorsAndType: (name) => colorsAndTypeResource.readJson(sanitizeFileName(name)),
1793
+ readComponentConfig: (comp, name) => readComponentConfig(comp, sanitizeFileName(name)),
1794
+ normalizeColorsAndType: (colorsAndType) => normalizeColorsAndType(colorsAndType)
1795
+ };
1796
+ function readTheme(fileName) {
1797
+ let raw;
1513
1798
  try {
1514
- manifest = JSON.parse(import_fs3.default.readFileSync(manifestPath, "utf-8"));
1799
+ raw = themesResource.readJson(fileName);
1800
+ } catch {
1801
+ return null;
1802
+ }
1803
+ if (!raw) return null;
1804
+ if (isColorsAndTypeShaped(raw)) return null;
1805
+ return normalizeTheme(raw, diskThemeResolvers);
1806
+ }
1807
+ function unreadableThemeReason(fileName) {
1808
+ const resolved = themesResource.existingPath(fileName);
1809
+ if (resolved === null) return "missing";
1810
+ try {
1811
+ if (isColorsAndTypeShaped(JSON.parse(import_fs4.default.readFileSync(resolved, "utf-8")))) {
1812
+ return "colors-and-type";
1813
+ }
1515
1814
  } catch {
1516
- return false;
1517
1815
  }
1518
- if (field === "theme") {
1519
- manifest.theme = fileName;
1520
- } else if (field === "component" && comp) {
1521
- manifest.componentConfigs = manifest.componentConfigs ?? {};
1522
- manifest.componentConfigs[comp] = fileName;
1816
+ return "corrupt";
1817
+ }
1818
+ function notAThemeError(fileName) {
1819
+ return `"${fileName}" is a colors-and-type file, not a theme. Before 0.48 those lived in themes/; move it to ${import_path4.default.basename(COLORS_AND_TYPE_DIR)}/ or delete it.`;
1820
+ }
1821
+ function respondUnreadableTheme(res, fileName, missingError) {
1822
+ switch (unreadableThemeReason(fileName)) {
1823
+ case "missing":
1824
+ jsonResponse(res, 404, { error: missingError });
1825
+ return;
1826
+ case "colors-and-type":
1827
+ jsonResponse(res, 422, { error: notAThemeError(fileName), code: "CORRUPT_THEME" });
1828
+ return;
1829
+ case "corrupt":
1830
+ jsonResponse(res, 422, {
1831
+ error: `Theme file "${fileName}" is not valid JSON`,
1832
+ code: "CORRUPT_THEME"
1833
+ });
1834
+ }
1835
+ }
1836
+ function writeTheme(fileName, theme) {
1837
+ themesResource.ensureDir();
1838
+ import_fs4.default.writeFileSync(themesResource.filePath(fileName), JSON.stringify(theme, null, 2));
1839
+ }
1840
+ function migrateLocalThemes(warn) {
1841
+ if (import_path4.default.resolve(THEMES_DIR) === import_path4.default.resolve(packageThemesDir)) return;
1842
+ if (!import_fs4.default.existsSync(THEMES_DIR)) return;
1843
+ for (const file of import_fs4.default.readdirSync(THEMES_DIR)) {
1844
+ if (!file.endsWith(".json") || file.startsWith("_")) continue;
1845
+ const fileName = file.slice(0, -".json".length);
1846
+ if (fileName === "default") continue;
1847
+ let raw;
1848
+ try {
1849
+ raw = JSON.parse(import_fs4.default.readFileSync(import_path4.default.join(THEMES_DIR, file), "utf-8"));
1850
+ } catch {
1851
+ continue;
1852
+ }
1853
+ if (isColorsAndTypeShaped(raw)) {
1854
+ warn(`[live-tokens] ${notAThemeError(fileName)} It is left untouched.`);
1855
+ continue;
1856
+ }
1857
+ const { theme, dropped, migrated } = normalizeTheme(raw, diskThemeResolvers);
1858
+ if (!migrated) continue;
1859
+ writeTheme(fileName, theme);
1860
+ if (dropped.length > 0) {
1861
+ warn(
1862
+ `[live-tokens] Theme "${fileName}": ${dropped.join(", ")} no longer exists; those slices now use their defaults.`
1863
+ );
1864
+ }
1523
1865
  }
1524
- manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1525
- import_fs3.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1526
- return true;
1866
+ }
1867
+ function ensureDefaultTheme(warn) {
1868
+ let existing = null;
1869
+ try {
1870
+ existing = JSON.parse(import_fs4.default.readFileSync(themesResource.filePath("default"), "utf-8"));
1871
+ } catch {
1872
+ }
1873
+ if (isColorsAndTypeShaped(existing)) {
1874
+ warn(`[live-tokens] ${notAThemeError("default")} The Default theme was not materialised.`);
1875
+ return;
1876
+ }
1877
+ const colorsAndType = normalizeColorsAndType(colorsAndTypeResource.readJson("default"));
1878
+ const componentConfigs = {};
1879
+ for (const comp of listComponentNames()) {
1880
+ const cfg = readComponentConfig(comp, "default");
1881
+ if (cfg) componentConfigs[comp] = cfg;
1882
+ }
1883
+ if (existing?.schemaVersion === THEME_SCHEMA_VERSION && JSON.stringify(existing.colorsAndType) === JSON.stringify(colorsAndType) && JSON.stringify(existing.componentConfigs) === JSON.stringify(componentConfigs)) {
1884
+ return;
1885
+ }
1886
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1887
+ writeTheme("default", {
1888
+ name: "Motion Proto",
1889
+ createdAt: typeof existing?.createdAt === "string" ? existing.createdAt : now,
1890
+ updatedAt: now,
1891
+ schemaVersion: THEME_SCHEMA_VERSION,
1892
+ colorsAndType,
1893
+ componentConfigs
1894
+ });
1895
+ }
1896
+ function ensureThemesDir(warn) {
1897
+ themesResource.ensureDir();
1898
+ ensureDefaultTheme(warn);
1899
+ migrateLocalThemes(warn);
1900
+ const productionPointerHeld = !import_fs4.default.existsSync(themesResource.productionPath) && listLegacyPointers().length > 0;
1901
+ if (productionPointerHeld) {
1902
+ if (!import_fs4.default.existsSync(themesResource.activePath)) themesResource.setActiveName("default");
1903
+ } else {
1904
+ themesResource.ensureMeta();
1905
+ }
1906
+ if (themesResource.existingPath(themesResource.getActiveName()) === null) {
1907
+ themesResource.setActiveName("default");
1908
+ }
1909
+ if (themesResource.existingPath(themesResource.getProductionName()) === null) {
1910
+ themesResource.setProductionName("default");
1911
+ }
1912
+ return { productionPointerHeld };
1913
+ }
1914
+ function activeTheme() {
1915
+ return readTheme(themesResource.getActiveName())?.theme ?? null;
1916
+ }
1917
+ function resolveLiveColorsAndType(theme) {
1918
+ const working = colorsAndTypeResource.readWorking();
1919
+ if (working !== null) return { data: normalizeColorsAndType(working), source: "working" };
1920
+ if (theme?.colorsAndType) return { data: theme.colorsAndType, source: "theme" };
1921
+ const shipped = colorsAndTypeResource.readJson("default");
1922
+ if (!shipped) return null;
1923
+ return { data: normalizeColorsAndType(shipped), source: "default" };
1924
+ }
1925
+ function resolveLiveComponentConfig(comp, theme) {
1926
+ const working = componentResource(comp).readWorking();
1927
+ if (working !== null) return { data: working, source: "working" };
1928
+ const embedded = theme?.componentConfigs[comp];
1929
+ if (embedded) return { data: { ...embedded, component: comp }, source: "theme" };
1930
+ const cfg = readComponentConfig(comp, "default");
1931
+ if (!cfg) return null;
1932
+ return { data: cfg, source: "default" };
1527
1933
  }
1528
1934
  function formatAliasGradient(v) {
1529
1935
  const stopColor = (s) => {
@@ -1559,237 +1965,191 @@ ${lines.join("\n")}
1559
1965
  return null;
1560
1966
  }
1561
1967
  }
1562
- function syncComponentsToCss() {
1563
- regenerateTokensCss();
1564
- }
1565
1968
  const escapedBase = API_BASE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1969
+ const COLORS_AND_TYPE_ROUTE = `${API_BASE}/colors-and-type`;
1970
+ const COLORS_AND_TYPE_ACTIVE_ROUTE = `${API_BASE}/colors-and-type/active`;
1971
+ const COLORS_AND_TYPE_PRODUCTION_ROUTE = `${API_BASE}/colors-and-type/production`;
1972
+ const COLORS_AND_TYPE_WORKING_ROUTE = `${API_BASE}/colors-and-type/working`;
1973
+ const COMPONENT_CONFIGS_ROUTE = `${API_BASE}/component-configs`;
1566
1974
  const THEMES_ROUTE = `${API_BASE}/themes`;
1567
1975
  const THEMES_ACTIVE_ROUTE = `${API_BASE}/themes/active`;
1568
1976
  const THEMES_PRODUCTION_ROUTE = `${API_BASE}/themes/production`;
1569
- const COMPONENT_CONFIGS_ROUTE = `${API_BASE}/component-configs`;
1570
- const MANIFESTS_ROUTE = `${API_BASE}/manifests`;
1571
- const MANIFESTS_ACTIVE_ROUTE = `${API_BASE}/manifests/active`;
1572
- const THEME_BY_NAME_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)$`);
1977
+ const COLORS_AND_TYPE_BY_NAME_REGEX = new RegExp(`^${escapedBase}/colors-and-type/([a-z0-9\\-_]+)$`);
1573
1978
  const COMP_LIST_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)$`);
1574
1979
  const COMP_ACTIVE_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/active$`);
1575
1980
  const COMP_PRODUCTION_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/production$`);
1981
+ const COMP_WORKING_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/working$`);
1576
1982
  const COMP_BY_NAME_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/([a-z0-9\\-_]+)$`);
1577
- const MANIFEST_APPLY_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/apply$`);
1578
- const MANIFEST_EXPORT_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/export$`);
1579
- const MANIFEST_IMPORT_ROUTE = `${API_BASE}/manifests/import`;
1580
- const MANIFEST_BY_NAME_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)$`);
1581
- async function handleListThemes(_ctx) {
1582
- const activeFile = themesResource.getActiveName();
1583
- const files = themesResource.listNames().map((fileName) => {
1584
- const data = themesResource.readJson(fileName);
1983
+ const THEME_APPLY_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)/apply$`);
1984
+ const THEME_EXPORT_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)/export$`);
1985
+ const THEME_IMPORT_ROUTE = `${API_BASE}/themes/import`;
1986
+ const THEME_BY_NAME_REGEX = new RegExp(`^${escapedBase}/themes/([a-z0-9\\-_]+)$`);
1987
+ const PRODUCTION_ROUTE = `${API_BASE}/production`;
1988
+ async function handleListColorsAndType(_ctx) {
1989
+ const files = colorsAndTypeResource.listNames().map((fileName) => {
1990
+ const data = colorsAndTypeResource.readJson(fileName);
1585
1991
  return {
1586
1992
  name: data?.name || fileName,
1587
1993
  fileName,
1588
1994
  updatedAt: data?.updatedAt || "",
1589
- isActive: fileName === activeFile
1995
+ // Package-owned rows are the shipped presets and the default. The Load
1996
+ // window offers those as whole themes, so it lists only the rows a
1997
+ // local file backs — including a local copy of a shipped name, which
1998
+ // holds whatever the user did to it.
1999
+ isPackage: colorsAndTypeResource.isPackageFile(fileName)
1590
2000
  };
1591
2001
  });
1592
2002
  jsonResponse(_ctx.res, 200, { files });
1593
2003
  }
1594
- async function handleGetActiveTheme({ res }) {
1595
- const activeFile = themesResource.getActiveName();
1596
- const raw = themesResource.readJson(activeFile);
1597
- if (!raw) {
1598
- jsonResponse(res, 404, { error: "Active theme not found" });
1599
- return;
1600
- }
1601
- const data = normalizeTheme(raw);
1602
- data._fileName = activeFile;
1603
- jsonResponse(res, 200, data);
1604
- }
1605
- async function handleSetActiveTheme({ req, res }) {
1606
- const body = JSON.parse(await readBody(req));
1607
- const fileName = sanitizeFileName(body.name || "default");
1608
- if (themesResource.existingPath(fileName) === null) {
1609
- jsonResponse(res, 404, { error: "Theme not found" });
1610
- return;
1611
- }
1612
- themesResource.setActiveName(fileName);
1613
- jsonResponse(res, 200, { ok: true, activeFile: fileName });
1614
- }
1615
- async function handleGetProductionTheme({ res }) {
1616
- const prodFile = themesResource.getProductionName();
1617
- const raw = themesResource.readJson(prodFile);
1618
- if (!raw) {
1619
- jsonResponse(res, 200, { fileName: prodFile, name: prodFile, cssVariables: {} });
2004
+ async function handleGetActiveColorsAndType({ res }) {
2005
+ const resolved = resolveLiveColorsAndType(activeTheme());
2006
+ if (!resolved) {
2007
+ jsonResponse(res, 404, { error: "Active colors and type not found" });
1620
2008
  return;
1621
2009
  }
1622
- const data = normalizeTheme(raw);
1623
2010
  jsonResponse(res, 200, {
1624
- fileName: prodFile,
1625
- name: data.name || prodFile,
1626
- updatedAt: data.updatedAt || "",
1627
- cssVariables: data.cssVariables || {}
2011
+ ...resolved.data,
2012
+ _fileName: themesResource.getActiveName(),
2013
+ _source: resolved.source
1628
2014
  });
1629
2015
  }
1630
- async function handleSetProductionTheme({ req, res }) {
1631
- const body = JSON.parse(await readBody(req));
1632
- const fileName = sanitizeFileName(body.name || "default");
1633
- if (themesResource.existingPath(fileName) === null) {
1634
- jsonResponse(res, 404, { error: "Theme not found" });
1635
- return;
2016
+ async function readBufferBody(req, res) {
2017
+ let parsed;
2018
+ try {
2019
+ parsed = JSON.parse(await readBody(req));
2020
+ } catch {
2021
+ parsed = void 0;
1636
2022
  }
1637
- if (manifestsResource.getActiveName() === "default") {
1638
- jsonResponse(res, 409, {
1639
- error: "Active manifest is protected. Save As first.",
1640
- code: "ACTIVE_IS_PROTECTED"
1641
- });
2023
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2024
+ jsonResponse(res, 400, { error: "Body must be a JSON object", code: "INVALID_BODY" });
2025
+ return null;
2026
+ }
2027
+ return parsed;
2028
+ }
2029
+ async function handleGetWorkingColorsAndType({ res }) {
2030
+ const raw = colorsAndTypeResource.readWorking();
2031
+ if (raw === null) {
2032
+ jsonResponse(res, 404, { error: "No working colors and type" });
1642
2033
  return;
1643
2034
  }
1644
- themesResource.setProductionName(fileName);
1645
- syncTokensToCss(fileName);
1646
- syncFontsToCss(fileName);
1647
- syncComponentsToCss();
1648
- patchActiveManifest("theme", null, fileName);
1649
- const data = themesResource.readJson(fileName);
1650
- jsonResponse(res, 200, {
1651
- ok: true,
1652
- fileName,
1653
- name: data?.name || fileName,
1654
- updatedAt: data?.updatedAt || ""
1655
- });
2035
+ jsonResponse(res, 200, normalizeColorsAndType(raw));
1656
2036
  }
1657
- async function handleThemeByName({ params, req, res }) {
2037
+ async function handleSetWorkingColorsAndType({ req, res }) {
2038
+ const body = await readBufferBody(req, res);
2039
+ if (!body) return;
2040
+ colorsAndTypeResource.writeWorking(body);
2041
+ jsonResponse(res, 200, { ok: true });
2042
+ }
2043
+ async function handleClearWorkingColorsAndType({ res }) {
2044
+ colorsAndTypeResource.clearWorking();
2045
+ jsonResponse(res, 200, { ok: true });
2046
+ }
2047
+ async function handleColorsAndTypeByName({ params, req, res }) {
1658
2048
  const [fileName] = params;
1659
- const filePath = themesResource.filePath(fileName);
2049
+ if (rejectReservedName(res, fileName)) return;
2050
+ const filePath = colorsAndTypeResource.filePath(fileName);
1660
2051
  if (req.method === "GET") {
1661
- const raw = themesResource.readJson(fileName);
2052
+ const raw = colorsAndTypeResource.readJson(fileName);
1662
2053
  if (!raw) {
1663
2054
  jsonResponse(res, 404, { error: "Not found" });
1664
2055
  return;
1665
2056
  }
1666
- const data = normalizeTheme(raw);
2057
+ const data = normalizeColorsAndType(raw);
1667
2058
  data._fileName = fileName;
1668
2059
  jsonResponse(res, 200, data);
1669
2060
  return;
1670
2061
  }
1671
2062
  if (req.method === "PUT") {
1672
2063
  if (fileName === "default") {
1673
- jsonResponse(res, 403, { error: "Cannot overwrite the default theme (live from package)" });
2064
+ jsonResponse(res, 403, { error: "Cannot overwrite the default colors and type (live from package)" });
1674
2065
  return;
1675
2066
  }
1676
2067
  const body = JSON.parse(await readBody(req));
1677
2068
  body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1678
- if (import_fs3.default.existsSync(filePath)) {
2069
+ if (import_fs4.default.existsSync(filePath)) {
1679
2070
  try {
1680
- const existing = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
2071
+ const existing = JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
1681
2072
  if (existing.createdAt) body.createdAt = existing.createdAt;
1682
2073
  } catch {
1683
2074
  }
1684
2075
  }
1685
2076
  if (!body.createdAt) body.createdAt = body.updatedAt;
1686
- import_fs3.default.writeFileSync(filePath, JSON.stringify(body, null, 2));
1687
- if (fileName === themesResource.getProductionName()) {
1688
- syncTokensToCss(fileName);
1689
- syncFontsToCss(fileName);
1690
- syncComponentsToCss();
1691
- }
2077
+ import_fs4.default.writeFileSync(filePath, JSON.stringify(body, null, 2));
1692
2078
  jsonResponse(res, 200, { ok: true, fileName });
1693
2079
  return;
1694
2080
  }
1695
2081
  if (req.method === "DELETE") {
1696
2082
  if (fileName === "default") {
1697
- jsonResponse(res, 403, { error: "Cannot delete the default theme" });
2083
+ jsonResponse(res, 403, { error: "Cannot delete the default colors and type" });
1698
2084
  return;
1699
2085
  }
1700
- if (themesResource.getProductionName() === fileName) {
2086
+ if (import_fs4.default.existsSync(filePath)) {
2087
+ import_fs4.default.unlinkSync(filePath);
2088
+ } else if (colorsAndTypeResource.existingPath(fileName)) {
1701
2089
  jsonResponse(res, 403, {
1702
- error: "Cannot delete the production theme. Adopt a different theme first.",
1703
- code: "PRODUCTION_THEME"
2090
+ error: "Cannot delete colors and type shipped with the package. Saving it creates a local copy; deleting that copy restores the shipped version.",
2091
+ code: "PACKAGE_COLORS_AND_TYPE"
1704
2092
  });
1705
2093
  return;
1706
2094
  }
1707
- if (import_fs3.default.existsSync(filePath)) {
1708
- import_fs3.default.unlinkSync(filePath);
1709
- if (themesResource.getActiveName() === fileName) {
1710
- themesResource.setActiveName("default");
1711
- }
1712
- }
1713
2095
  jsonResponse(res, 200, { ok: true });
1714
2096
  return;
1715
2097
  }
1716
2098
  }
1717
2099
  async function handleListComponents({ res }) {
1718
- const components = listComponentNames().map((comp) => {
1719
- const r = componentResource(comp);
1720
- return {
1721
- name: comp,
1722
- activeFile: r.getActiveName(),
1723
- productionFile: r.getProductionName()
1724
- };
1725
- });
2100
+ const theme = activeTheme();
2101
+ const components = listComponentNames().map((comp) => ({
2102
+ name: comp,
2103
+ // Presence, not a parse: one corrupt buffer must not take the listing
2104
+ // down, and the door only has to say where each component's live config
2105
+ // comes from.
2106
+ source: componentResource(comp).hasWorking() ? "working" : theme?.componentConfigs[comp] ? "theme" : "default"
2107
+ }));
1726
2108
  jsonResponse(res, 200, { components });
1727
2109
  }
2110
+ function rejectUnknownComponent(res, comp) {
2111
+ if (listComponentNames().includes(comp)) return false;
2112
+ jsonResponse(res, 404, { error: `Unknown component "${comp}"`, code: "UNKNOWN_COMPONENT" });
2113
+ return true;
2114
+ }
1728
2115
  async function handleGetComponentActive({ params, res }) {
1729
2116
  const [comp] = params;
1730
- const r = componentResource(comp);
1731
- const activeFile = r.getActiveName();
1732
- const cfg = readComponentConfig(comp, activeFile);
1733
- if (!cfg) {
2117
+ const resolved = resolveLiveComponentConfig(comp, activeTheme());
2118
+ if (!resolved) {
1734
2119
  jsonResponse(res, 404, { error: "Active config not found" });
1735
2120
  return;
1736
2121
  }
1737
- jsonResponse(res, 200, { ...cfg, _fileName: activeFile });
2122
+ jsonResponse(res, 200, {
2123
+ ...resolved.data,
2124
+ _fileName: themesResource.getActiveName(),
2125
+ _source: resolved.source
2126
+ });
1738
2127
  }
1739
- async function handleSetComponentActive({ params, req, res }) {
2128
+ async function handleGetComponentWorking({ params, res }) {
1740
2129
  const [comp] = params;
1741
- const body = JSON.parse(await readBody(req));
1742
- const fileName = sanitizeFileName(body.name || "default");
1743
- const r = componentResource(comp);
1744
- if (r.existingPath(fileName) === null) {
1745
- jsonResponse(res, 404, { error: "Config not found" });
2130
+ const raw = componentResource(comp).readWorking();
2131
+ if (raw === null) {
2132
+ jsonResponse(res, 404, { error: "No working config" });
1746
2133
  return;
1747
2134
  }
1748
- r.ensureDir();
1749
- r.setActiveName(fileName);
1750
- jsonResponse(res, 200, { ok: true, activeFile: fileName });
2135
+ jsonResponse(res, 200, raw);
1751
2136
  }
1752
- async function handleGetComponentProduction({ params, res }) {
2137
+ async function handleSetComponentWorking({ params, req, res }) {
1753
2138
  const [comp] = params;
1754
- const r = componentResource(comp);
1755
- const prodFile = r.getProductionName();
1756
- const cfg = readComponentConfig(comp, prodFile);
1757
- jsonResponse(res, 200, {
1758
- fileName: prodFile,
1759
- name: cfg?.name || prodFile,
1760
- aliases: cfg?.aliases || {}
1761
- });
2139
+ if (rejectUnknownComponent(res, comp)) return;
2140
+ const body = await readBufferBody(req, res);
2141
+ if (!body) return;
2142
+ componentResource(comp).writeWorking(body);
2143
+ jsonResponse(res, 200, { ok: true });
1762
2144
  }
1763
- async function handleSetComponentProduction({ params, req, res }) {
2145
+ async function handleClearComponentWorking({ params, res }) {
1764
2146
  const [comp] = params;
1765
- const body = JSON.parse(await readBody(req));
1766
- const fileName = sanitizeFileName(body.name || "default");
1767
- const r = componentResource(comp);
1768
- if (r.existingPath(fileName) === null) {
1769
- jsonResponse(res, 404, { error: "Config not found" });
1770
- return;
1771
- }
1772
- if (manifestsResource.getActiveName() === "default") {
1773
- jsonResponse(res, 409, {
1774
- error: "Active manifest is protected. Save As first.",
1775
- code: "ACTIVE_IS_PROTECTED"
1776
- });
1777
- return;
1778
- }
1779
- r.ensureDir();
1780
- r.setProductionName(fileName);
1781
- syncComponentsToCss();
1782
- patchActiveManifest("component", comp, fileName);
1783
- const cfg = readComponentConfig(comp, fileName);
1784
- jsonResponse(res, 200, {
1785
- ok: true,
1786
- fileName,
1787
- name: cfg?.name || fileName,
1788
- aliases: cfg?.aliases || {}
1789
- });
2147
+ componentResource(comp).clearWorking();
2148
+ jsonResponse(res, 200, { ok: true });
1790
2149
  }
1791
2150
  async function handleComponentConfigByName({ params, req, res }) {
1792
2151
  const [comp, name] = params;
2152
+ if (rejectReservedName(res, name)) return;
1793
2153
  const r = componentResource(comp);
1794
2154
  const configPath = r.filePath(name);
1795
2155
  if (req.method === "GET") {
@@ -1812,19 +2172,16 @@ ${lines.join("\n")}
1812
2172
  body.component = comp;
1813
2173
  body.name = body.name || name;
1814
2174
  body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1815
- if (import_fs3.default.existsSync(configPath)) {
2175
+ if (import_fs4.default.existsSync(configPath)) {
1816
2176
  try {
1817
- const existing = JSON.parse(import_fs3.default.readFileSync(configPath, "utf-8"));
2177
+ const existing = JSON.parse(import_fs4.default.readFileSync(configPath, "utf-8"));
1818
2178
  if (existing.createdAt) body.createdAt = existing.createdAt;
1819
2179
  } catch {
1820
2180
  }
1821
2181
  }
1822
2182
  if (!body.createdAt) body.createdAt = body.updatedAt;
1823
2183
  r.ensureDir();
1824
- import_fs3.default.writeFileSync(configPath, JSON.stringify(body, null, 2));
1825
- if (r.getProductionName() === name) {
1826
- syncComponentsToCss();
1827
- }
2184
+ import_fs4.default.writeFileSync(configPath, JSON.stringify(body, null, 2));
1828
2185
  jsonResponse(res, 200, { ok: true, fileName: name });
1829
2186
  return;
1830
2187
  }
@@ -1833,16 +2190,7 @@ ${lines.join("\n")}
1833
2190
  jsonResponse(res, 403, { error: "Cannot delete default config" });
1834
2191
  return;
1835
2192
  }
1836
- if (import_fs3.default.existsSync(configPath)) {
1837
- import_fs3.default.unlinkSync(configPath);
1838
- if (r.getActiveName() === name) {
1839
- r.setActiveName("default");
1840
- }
1841
- if (r.getProductionName() === name) {
1842
- r.setProductionName("default");
1843
- syncComponentsToCss();
1844
- }
1845
- }
2193
+ if (import_fs4.default.existsSync(configPath)) import_fs4.default.unlinkSync(configPath);
1846
2194
  jsonResponse(res, 200, { ok: true });
1847
2195
  return;
1848
2196
  }
@@ -1850,199 +2198,190 @@ ${lines.join("\n")}
1850
2198
  async function handleListComponentConfigs({ params, res }) {
1851
2199
  const [comp] = params;
1852
2200
  const r = componentResource(comp);
1853
- if (!import_fs3.default.existsSync(r.dir)) {
2201
+ if (!import_fs4.default.existsSync(r.dir)) {
1854
2202
  jsonResponse(res, 404, { error: "Component not found" });
1855
2203
  return;
1856
2204
  }
1857
- const activeFile = r.getActiveName();
1858
- const productionFile = r.getProductionName();
1859
2205
  const files = r.listNames().map((fileName) => {
1860
2206
  const data = r.readJson(fileName);
1861
2207
  return {
1862
2208
  name: data?.name || fileName,
1863
2209
  fileName,
1864
- updatedAt: data?.updatedAt || "",
1865
- isActive: fileName === activeFile,
1866
- isProduction: fileName === productionFile
2210
+ updatedAt: data?.updatedAt || ""
1867
2211
  };
1868
2212
  });
1869
- jsonResponse(res, 200, { component: comp, files, activeFile, productionFile });
2213
+ jsonResponse(res, 200, { component: comp, files });
1870
2214
  }
1871
- async function handleListManifests({ res }) {
1872
- const activeFile = manifestsResource.getActiveName();
1873
- const files = manifestsResource.listNames().map((fileName) => {
1874
- const data = manifestsResource.readJson(fileName);
1875
- return {
1876
- name: data?.name || fileName,
2215
+ async function handleListThemes({ res }) {
2216
+ const activeFile = themesResource.getActiveName();
2217
+ const productionFile = themesResource.getProductionName();
2218
+ const files = [];
2219
+ for (const fileName of themesResource.listNames()) {
2220
+ const read = readTheme(fileName);
2221
+ if (!read) {
2222
+ console.warn(
2223
+ 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.`
2224
+ );
2225
+ continue;
2226
+ }
2227
+ files.push({
2228
+ name: read.theme.name,
1877
2229
  fileName,
1878
- updatedAt: data?.updatedAt || "",
2230
+ updatedAt: read.theme.updatedAt,
1879
2231
  isActive: fileName === activeFile,
2232
+ isProduction: fileName === productionFile,
1880
2233
  isProtected: fileName === "default"
1881
- };
1882
- });
1883
- jsonResponse(res, 200, { files, activeFile });
2234
+ });
2235
+ }
2236
+ jsonResponse(res, 200, { files, activeFile, productionFile });
1884
2237
  }
1885
- async function handleGetActiveManifest({ res }) {
1886
- const activeFile = manifestsResource.getActiveName();
1887
- const raw = manifestsResource.readJson(activeFile);
1888
- if (!raw) {
1889
- jsonResponse(res, 404, { error: "Active manifest not found" });
2238
+ async function handleGetProductionTheme({ res }) {
2239
+ const fileName = themesResource.getProductionName();
2240
+ const read = readTheme(fileName);
2241
+ if (!read) {
2242
+ respondUnreadableTheme(res, fileName, "Production theme not found");
1890
2243
  return;
1891
2244
  }
1892
- const data = raw;
1893
- data._fileName = activeFile;
1894
- jsonResponse(res, 200, data);
2245
+ jsonResponse(res, 200, { ...read.theme, _fileName: fileName });
1895
2246
  }
1896
- async function handleSetActiveManifest({ req, res }) {
2247
+ async function handleGetActiveTheme({ res }) {
2248
+ const activeFile = themesResource.getActiveName();
2249
+ const read = readTheme(activeFile);
2250
+ if (!read) {
2251
+ respondUnreadableTheme(res, activeFile, "Active theme not found");
2252
+ return;
2253
+ }
2254
+ jsonResponse(res, 200, { ...read.theme, _fileName: activeFile });
2255
+ }
2256
+ async function handleSetActiveTheme({ req, res }) {
1897
2257
  const body = JSON.parse(await readBody(req));
1898
2258
  const fileName = sanitizeFileName(body.name || "default");
1899
- if (manifestsResource.existingPath(fileName) === null) {
1900
- jsonResponse(res, 404, { error: "Manifest not found" });
2259
+ if (themesResource.existingPath(fileName) === null) {
2260
+ jsonResponse(res, 404, { error: "Theme not found" });
1901
2261
  return;
1902
2262
  }
1903
- manifestsResource.setActiveName(fileName);
2263
+ themesResource.setActiveName(fileName);
1904
2264
  jsonResponse(res, 200, { ok: true, activeFile: fileName });
1905
2265
  }
1906
- async function handleManifestByName({ params, req, res }) {
2266
+ async function handleThemeByName({ params, req, res }) {
1907
2267
  const [fileName] = params;
1908
- const filePath = manifestsResource.filePath(fileName);
2268
+ if (rejectReservedName(res, fileName)) return;
2269
+ const filePath = themesResource.filePath(fileName);
1909
2270
  if (req.method === "GET") {
1910
- const raw = manifestsResource.readJson(fileName);
1911
- if (!raw) {
1912
- jsonResponse(res, 404, { error: "Not found" });
2271
+ const read = readTheme(fileName);
2272
+ if (!read) {
2273
+ respondUnreadableTheme(res, fileName, "Not found");
1913
2274
  return;
1914
2275
  }
1915
- const data = raw;
1916
- data._fileName = fileName;
1917
- jsonResponse(res, 200, data);
2276
+ jsonResponse(res, 200, { ...read.theme, _fileName: fileName });
1918
2277
  return;
1919
2278
  }
1920
2279
  if (req.method === "PUT") {
1921
2280
  if (fileName === "default") {
1922
- jsonResponse(res, 403, { error: "Cannot overwrite the default manifest" });
2281
+ jsonResponse(res, 403, { error: "Cannot overwrite the default theme" });
1923
2282
  return;
1924
2283
  }
1925
2284
  const body = JSON.parse(await readBody(req));
1926
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1927
- if (import_fs3.default.existsSync(filePath)) {
2285
+ const { theme } = normalizeTheme(body, diskThemeResolvers);
2286
+ theme.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2287
+ if (import_fs4.default.existsSync(filePath)) {
1928
2288
  try {
1929
- const existing = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1930
- if (existing.createdAt) body.createdAt = existing.createdAt;
2289
+ const existing = JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
2290
+ if (existing.createdAt) theme.createdAt = existing.createdAt;
1931
2291
  } catch {
1932
2292
  }
1933
2293
  }
1934
- if (!body.createdAt) body.createdAt = body.updatedAt;
1935
- import_fs3.default.writeFileSync(filePath, JSON.stringify(body, null, 2));
2294
+ writeTheme(fileName, theme);
1936
2295
  jsonResponse(res, 200, { ok: true, fileName });
1937
2296
  return;
1938
2297
  }
1939
2298
  if (req.method === "DELETE") {
1940
2299
  if (fileName === "default") {
1941
- jsonResponse(res, 403, { error: "Cannot delete the default manifest" });
2300
+ jsonResponse(res, 403, { error: "Cannot delete the default theme" });
2301
+ return;
2302
+ }
2303
+ if (fileName === themesResource.getProductionName()) {
2304
+ jsonResponse(res, 403, {
2305
+ error: "Cannot delete the theme that is in production. Adopt another theme first.",
2306
+ code: "PRODUCTION_THEME"
2307
+ });
1942
2308
  return;
1943
2309
  }
1944
- if (import_fs3.default.existsSync(filePath)) {
1945
- import_fs3.default.unlinkSync(filePath);
1946
- if (manifestsResource.getActiveName() === fileName) {
1947
- manifestsResource.setActiveName("default");
2310
+ if (import_fs4.default.existsSync(filePath)) {
2311
+ import_fs4.default.unlinkSync(filePath);
2312
+ if (themesResource.existingPath(fileName) === null && themesResource.getActiveName() === fileName) {
2313
+ themesResource.setActiveName("default");
1948
2314
  }
2315
+ } else if (themesResource.existingPath(fileName)) {
2316
+ jsonResponse(res, 403, {
2317
+ error: "Cannot delete a theme shipped with the package. Saving it creates a local copy; deleting that copy restores the shipped version.",
2318
+ code: "PACKAGE_THEME"
2319
+ });
2320
+ return;
1949
2321
  }
1950
2322
  jsonResponse(res, 200, { ok: true });
1951
2323
  return;
1952
2324
  }
1953
2325
  }
1954
- async function handleApplyManifest({ params, res }) {
2326
+ async function handleApplyTheme({ params, res }) {
1955
2327
  const [fileName] = params;
1956
- const manifest = manifestsResource.readJson(fileName);
1957
- if (!manifest) {
1958
- jsonResponse(res, 404, { error: "Manifest not found" });
2328
+ if (rejectReservedName(res, fileName)) return;
2329
+ const read = readTheme(fileName);
2330
+ if (!read) {
2331
+ respondUnreadableTheme(res, fileName, "Theme not found");
1959
2332
  return;
1960
2333
  }
1961
- const themeName = sanitizeFileName(manifest.theme || "default");
1962
- if (themesResource.existingPath(themeName) === null) {
1963
- jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
2334
+ const { theme } = read;
2335
+ const isDefault = fileName === "default";
2336
+ if (!isDefault && !theme.colorsAndType) {
2337
+ jsonResponse(res, 422, { error: "This theme carries no colors and type" });
1964
2338
  return;
1965
2339
  }
1966
- const knownComponents = new Set(listComponentNames());
1967
- const componentConfigs = manifest.componentConfigs ?? {};
1968
- const resolvedConfigs = {};
1969
- const apply = [];
1970
- for (const [comp, configFile] of Object.entries(componentConfigs)) {
1971
- if (!knownComponents.has(comp)) continue;
1972
- const sanitized = sanitizeFileName(String(configFile) || "default");
1973
- const r = componentResource(comp);
1974
- if (r.existingPath(sanitized) === null) {
1975
- jsonResponse(res, 422, {
1976
- error: `Manifest references missing config: ${comp}/${sanitized}`
1977
- });
1978
- return;
1979
- }
1980
- apply.push([comp, sanitized]);
1981
- }
1982
- themesResource.setActiveName(themeName);
1983
- themesResource.setProductionName(themeName);
1984
- syncTokensToCss(themeName);
1985
- syncFontsToCss(themeName);
1986
- const themeData = normalizeTheme(themesResource.readJson(themeName));
1987
- themeData._fileName = themeName;
1988
- for (const [comp, configFile] of apply) {
2340
+ if (isDefault) colorsAndTypeResource.clearWorking();
2341
+ else colorsAndTypeResource.writeWorking(theme.colorsAndType);
2342
+ const knownComponents = listComponentNames();
2343
+ for (const comp of knownComponents) {
2344
+ const embedded = isDefault ? void 0 : theme.componentConfigs[comp];
1989
2345
  const r = componentResource(comp);
1990
- r.setActiveName(configFile);
1991
- r.setProductionName(configFile);
1992
- const cfg = readComponentConfig(comp, configFile);
1993
- if (cfg) resolvedConfigs[comp] = { ...cfg, _fileName: configFile };
2346
+ if (embedded) r.writeWorking({ ...embedded, component: comp });
2347
+ else r.clearWorking();
1994
2348
  }
2349
+ themesResource.setActiveName(fileName);
2350
+ const resolvedConfigs = {};
1995
2351
  for (const comp of knownComponents) {
1996
- if (resolvedConfigs[comp]) continue;
1997
- const activeName = componentResource(comp).getActiveName();
1998
- const cfg = readComponentConfig(comp, activeName);
1999
- if (cfg) resolvedConfigs[comp] = { ...cfg, _fileName: activeName };
2352
+ const resolved = resolveLiveComponentConfig(comp, theme);
2353
+ if (resolved) resolvedConfigs[comp] = { ...resolved.data, _fileName: fileName, _source: resolved.source };
2000
2354
  }
2001
- syncComponentsToCss();
2002
- manifestsResource.setActiveName(fileName);
2355
+ const resolvedColorsAndType = resolveLiveColorsAndType(theme);
2356
+ const skippedComponents = Object.keys(theme.componentConfigs).filter(
2357
+ (comp) => !knownComponents.includes(comp)
2358
+ );
2003
2359
  jsonResponse(res, 200, {
2004
2360
  ok: true,
2005
- manifest: { ...manifest, _fileName: fileName },
2006
- theme: themeData,
2007
- componentConfigs: resolvedConfigs
2361
+ theme: { ...theme, _fileName: fileName },
2362
+ colorsAndType: resolvedColorsAndType && {
2363
+ ...resolvedColorsAndType.data,
2364
+ _fileName: fileName,
2365
+ _source: resolvedColorsAndType.source
2366
+ },
2367
+ componentConfigs: resolvedConfigs,
2368
+ skippedComponents
2008
2369
  });
2009
2370
  }
2010
- async function handleExportManifest({ params, res }) {
2371
+ async function handleExportTheme({ params, res }) {
2011
2372
  const [fileName] = params;
2012
- const manifest = manifestsResource.readJson(fileName);
2013
- if (!manifest) {
2014
- jsonResponse(res, 404, { error: "Manifest not found" });
2373
+ if (rejectReservedName(res, fileName)) return;
2374
+ const read = readTheme(fileName);
2375
+ if (!read) {
2376
+ respondUnreadableTheme(res, fileName, "Theme not found");
2015
2377
  return;
2016
2378
  }
2017
- const themeName = sanitizeFileName(manifest.theme || "default");
2018
- const theme = themesResource.readJson(themeName);
2019
- if (!theme) {
2020
- jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
2021
- return;
2022
- }
2023
- const knownComponents = new Set(listComponentNames());
2024
- const componentConfigs = {};
2025
- for (const [comp, configFile] of Object.entries(manifest.componentConfigs ?? {})) {
2026
- if (!knownComponents.has(comp)) continue;
2027
- const sanitized = sanitizeFileName(String(configFile) || "default");
2028
- if (sanitized === "default") continue;
2029
- const cfg = componentResource(comp).readJson(sanitized);
2030
- if (!cfg) {
2031
- jsonResponse(res, 422, {
2032
- error: `Manifest references missing config: ${comp}/${sanitized}`
2033
- });
2034
- return;
2035
- }
2036
- componentConfigs[`${comp}/${sanitized}`] = cfg;
2037
- }
2038
2379
  const bundle = {
2039
- kind: "manifest-bundle",
2040
- schemaVersion: 1,
2380
+ kind: "theme-bundle",
2381
+ schemaVersion: THEME_SCHEMA_VERSION,
2041
2382
  liveTokensVersion: PKG_VERSION,
2042
2383
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2043
- manifest,
2044
- theme,
2045
- componentConfigs
2384
+ manifest: read.theme
2046
2385
  };
2047
2386
  res.statusCode = 200;
2048
2387
  res.setHeader("Content-Type", "application/json");
@@ -2055,7 +2394,7 @@ ${lines.join("\n")}
2055
2394
  function nextAvailableName2(exists, baseName) {
2056
2395
  return nextAvailableName(exists, sanitizeFileName(baseName));
2057
2396
  }
2058
- async function handleImportManifest({ req, res }) {
2397
+ async function handleImportTheme({ req, res }) {
2059
2398
  let bundle;
2060
2399
  try {
2061
2400
  bundle = JSON.parse(await readBody(req));
@@ -2063,117 +2402,112 @@ ${lines.join("\n")}
2063
2402
  jsonResponse(res, 400, { error: "Body is not valid JSON" });
2064
2403
  return;
2065
2404
  }
2066
- if (!bundle || bundle.kind !== "manifest-bundle") {
2405
+ if (!bundle || !Object.values(BUNDLE_KIND_BY_VERSION).includes(bundle.kind)) {
2067
2406
  jsonResponse(res, 400, {
2068
- error: "Not a manifest bundle (kind discriminator missing or wrong)"
2407
+ error: "Not a theme bundle (kind discriminator missing or wrong)"
2069
2408
  });
2070
2409
  return;
2071
2410
  }
2072
- if (bundle.schemaVersion !== 1) {
2411
+ if (bundle.kind !== BUNDLE_KIND_BY_VERSION[bundle.schemaVersion]) {
2073
2412
  jsonResponse(res, 400, {
2074
- error: `Unsupported bundle schemaVersion: ${bundle.schemaVersion}`
2413
+ error: `Unsupported bundle: kind "${bundle.kind}" with schemaVersion ${bundle.schemaVersion}`
2075
2414
  });
2076
2415
  return;
2077
2416
  }
2078
- if (!bundle.manifest || !bundle.theme || !bundle.componentConfigs) {
2079
- jsonResponse(res, 400, { error: "Bundle missing manifest / theme / componentConfigs" });
2417
+ if (!bundle.manifest) {
2418
+ jsonResponse(res, 400, { error: "Bundle missing theme" });
2080
2419
  return;
2081
2420
  }
2421
+ const bundleResolvers = {
2422
+ readColorsAndType: () => bundle.theme,
2423
+ readComponentConfig: (comp, name) => bundle.componentConfigs?.[`${comp}/${name}`],
2424
+ normalizeColorsAndType: (colorsAndType) => normalizeColorsAndType(colorsAndType)
2425
+ };
2426
+ const { theme, dropped } = normalizeTheme(bundle.manifest, bundleResolvers);
2427
+ if (theme.colorsAndType) theme.colorsAndType = normalizeColorsAndType(theme.colorsAndType);
2428
+ if (!theme.colorsAndType) {
2429
+ jsonResponse(res, 400, { error: "Bundle carries no theme" });
2430
+ return;
2431
+ }
2432
+ theme.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2082
2433
  const renames = {};
2083
- const now = (/* @__PURE__ */ new Date()).toISOString();
2084
- const originalThemeName = sanitizeFileName(bundle.manifest.theme || "default");
2085
- const finalThemeName = nextAvailableName2(
2434
+ const originalName = sanitizeFileName(theme.name || "imported");
2435
+ const finalName = nextAvailableName2(
2086
2436
  (n) => themesResource.existingPath(n) !== null,
2087
- originalThemeName
2437
+ originalName
2088
2438
  );
2089
- if (finalThemeName !== originalThemeName) {
2090
- renames[`theme:${originalThemeName}`] = finalThemeName;
2439
+ if (finalName !== originalName) {
2440
+ renames[`theme:${originalName}`] = finalName;
2091
2441
  }
2092
- const themeBody = { ...bundle.theme };
2093
- themeBody.updatedAt = now;
2094
- if (!themeBody.createdAt) themeBody.createdAt = now;
2095
- themesResource.ensureDir();
2096
- import_fs3.default.writeFileSync(themesResource.filePath(finalThemeName), JSON.stringify(themeBody, null, 2));
2097
- const knownComponents = new Set(listComponentNames());
2098
- const componentRenames = {};
2099
- for (const [key, cfgValue] of Object.entries(bundle.componentConfigs)) {
2100
- const [comp, originalName] = key.split("/");
2101
- if (!comp || !originalName) continue;
2102
- if (!knownComponents.has(comp)) continue;
2103
- const r = componentResource(comp);
2104
- const finalName = nextAvailableName2(
2105
- (n) => r.existingPath(n) !== null,
2106
- originalName
2107
- );
2108
- if (finalName !== originalName) {
2109
- renames[`componentConfig:${comp}/${originalName}`] = finalName;
2110
- }
2111
- componentRenames[`${comp}/${originalName}`] = finalName;
2112
- const cfgBody = { ...cfgValue };
2113
- cfgBody.component = comp;
2114
- cfgBody.name = finalName;
2115
- cfgBody.updatedAt = now;
2116
- if (!cfgBody.createdAt) cfgBody.createdAt = now;
2117
- r.ensureDir();
2118
- import_fs3.default.writeFileSync(r.filePath(finalName), JSON.stringify(cfgBody, null, 2));
2442
+ writeTheme(finalName, theme);
2443
+ jsonResponse(res, 200, {
2444
+ ok: true,
2445
+ theme: finalName,
2446
+ renames,
2447
+ dropped
2448
+ });
2449
+ }
2450
+ async function handleAdoptLook({ res }) {
2451
+ const activeName = themesResource.getActiveName();
2452
+ if (activeName === "default") {
2453
+ jsonResponse(res, 409, {
2454
+ error: "Active theme is protected. Save As first.",
2455
+ code: "ACTIVE_IS_PROTECTED"
2456
+ });
2457
+ return;
2119
2458
  }
2120
- const rewrittenManifest = {
2121
- ...bundle.manifest,
2122
- theme: finalThemeName,
2123
- componentConfigs: {}
2124
- };
2125
- for (const [comp, configName] of Object.entries(bundle.manifest.componentConfigs ?? {})) {
2126
- const original = String(configName);
2127
- if (original === "default") {
2128
- rewrittenManifest.componentConfigs[comp] = "default";
2129
- continue;
2130
- }
2131
- const finalName = componentRenames[`${comp}/${original}`] ?? original;
2132
- rewrittenManifest.componentConfigs[comp] = finalName;
2133
- }
2134
- rewrittenManifest.updatedAt = now;
2135
- if (!rewrittenManifest.createdAt) rewrittenManifest.createdAt = now;
2136
- const originalManifestName = sanitizeFileName(bundle.manifest.name || "imported");
2137
- const finalManifestName = nextAvailableName2(
2138
- (n) => manifestsResource.existingPath(n) !== null,
2139
- originalManifestName
2140
- );
2141
- if (finalManifestName !== originalManifestName) {
2142
- renames[`manifest:${originalManifestName}`] = finalManifestName;
2459
+ const read = readTheme(activeName);
2460
+ if (!read) {
2461
+ respondUnreadableTheme(res, activeName, "Active theme not found");
2462
+ return;
2143
2463
  }
2144
- manifestsResource.ensureDir();
2145
- import_fs3.default.writeFileSync(
2146
- manifestsResource.filePath(finalManifestName),
2147
- JSON.stringify(rewrittenManifest, null, 2)
2148
- );
2464
+ themesResource.setProductionName(activeName);
2465
+ regenerateTokensCss();
2466
+ syncFontsToCss(read.theme.colorsAndType);
2149
2467
  jsonResponse(res, 200, {
2150
2468
  ok: true,
2151
- manifest: finalManifestName,
2152
- renames
2469
+ productionTheme: {
2470
+ fileName: activeName,
2471
+ name: read.theme.name,
2472
+ updatedAt: read.theme.updatedAt
2473
+ }
2153
2474
  });
2154
2475
  }
2155
2476
  function methodNotAllowed({ res }) {
2156
2477
  jsonResponse(res, 405, { error: "Method not allowed" });
2157
2478
  }
2479
+ let legacyLayout = null;
2158
2480
  const routes = [
2159
- // Themes — list / active / production are exact strings, must run before THEME_BY_NAME_REGEX
2160
- { method: "GET", pattern: THEMES_ROUTE, handler: handleListThemes },
2161
- { method: "GET", pattern: THEMES_ACTIVE_ROUTE, handler: handleGetActiveTheme },
2162
- { method: "PUT", pattern: THEMES_ACTIVE_ROUTE, handler: handleSetActiveTheme },
2163
- { method: "GET", pattern: THEMES_PRODUCTION_ROUTE, handler: handleGetProductionTheme },
2164
- { method: "PUT", pattern: THEMES_PRODUCTION_ROUTE, handler: handleSetProductionTheme },
2481
+ // Colors and type — list / active / production / working are exact strings;
2482
+ // they must run before COLORS_AND_TYPE_BY_NAME_REGEX
2483
+ { method: "GET", pattern: COLORS_AND_TYPE_ROUTE, handler: handleListColorsAndType },
2484
+ { method: "GET", pattern: COLORS_AND_TYPE_ACTIVE_ROUTE, handler: handleGetActiveColorsAndType },
2485
+ { method: "PUT", pattern: COLORS_AND_TYPE_ACTIVE_ROUTE, handler: methodNotAllowed },
2486
+ { method: "GET", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
2487
+ { method: "PUT", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
2488
+ { method: "POST", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
2489
+ { method: "DELETE", pattern: COLORS_AND_TYPE_PRODUCTION_ROUTE, handler: methodNotAllowed },
2490
+ { method: "GET", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: handleGetWorkingColorsAndType },
2491
+ { method: "PUT", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: handleSetWorkingColorsAndType },
2492
+ { method: "DELETE", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: handleClearWorkingColorsAndType },
2493
+ { method: "POST", pattern: COLORS_AND_TYPE_WORKING_ROUTE, handler: methodNotAllowed },
2165
2494
  // Component configs — list of components
2166
2495
  { method: "GET", pattern: COMPONENT_CONFIGS_ROUTE, handler: handleListComponents },
2167
2496
  // Component configs — :comp/active (must precede :comp/:name)
2168
2497
  { method: "GET", pattern: COMP_ACTIVE_REGEX, handler: handleGetComponentActive },
2169
- { method: "PUT", pattern: COMP_ACTIVE_REGEX, handler: handleSetComponentActive },
2498
+ { method: "PUT", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
2170
2499
  { method: "POST", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
2171
2500
  { method: "DELETE", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
2172
- // Component configs — :comp/production (must precede :comp/:name)
2173
- { method: "GET", pattern: COMP_PRODUCTION_REGEX, handler: handleGetComponentProduction },
2174
- { method: "PUT", pattern: COMP_PRODUCTION_REGEX, handler: handleSetComponentProduction },
2501
+ // Component configs — :comp/production, retired with the per-layer pointers
2502
+ { method: "GET", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
2503
+ { method: "PUT", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
2175
2504
  { method: "POST", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
2176
2505
  { method: "DELETE", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
2506
+ // Component configs — :comp/working (must precede :comp/:name)
2507
+ { method: "GET", pattern: COMP_WORKING_REGEX, handler: handleGetComponentWorking },
2508
+ { method: "PUT", pattern: COMP_WORKING_REGEX, handler: handleSetComponentWorking },
2509
+ { method: "DELETE", pattern: COMP_WORKING_REGEX, handler: handleClearComponentWorking },
2510
+ { method: "POST", pattern: COMP_WORKING_REGEX, handler: methodNotAllowed },
2177
2511
  // Component configs — :comp/:name (must precede :comp listing)
2178
2512
  { method: "GET", pattern: COMP_BY_NAME_REGEX, handler: handleComponentConfigByName },
2179
2513
  { method: "PUT", pattern: COMP_BY_NAME_REGEX, handler: handleComponentConfigByName },
@@ -2181,33 +2515,44 @@ ${lines.join("\n")}
2181
2515
  { method: "POST", pattern: COMP_BY_NAME_REGEX, handler: methodNotAllowed },
2182
2516
  // Component configs — list configs for :comp (broadest, runs last among comp routes)
2183
2517
  { method: "GET", pattern: COMP_LIST_REGEX, handler: handleListComponentConfigs },
2518
+ // Colors and type — :name CRUD (broadest colors-and-type route, runs last)
2519
+ { method: "GET", pattern: COLORS_AND_TYPE_BY_NAME_REGEX, handler: handleColorsAndTypeByName },
2520
+ { method: "PUT", pattern: COLORS_AND_TYPE_BY_NAME_REGEX, handler: handleColorsAndTypeByName },
2521
+ { method: "DELETE", pattern: COLORS_AND_TYPE_BY_NAME_REGEX, handler: handleColorsAndTypeByName },
2522
+ // Themes — list / active / production are exact strings, must run before regexes
2523
+ { method: "GET", pattern: THEMES_ROUTE, handler: handleListThemes },
2524
+ { method: "GET", pattern: THEMES_ACTIVE_ROUTE, handler: handleGetActiveTheme },
2525
+ { method: "PUT", pattern: THEMES_ACTIVE_ROUTE, handler: handleSetActiveTheme },
2526
+ { method: "GET", pattern: THEMES_PRODUCTION_ROUTE, handler: handleGetProductionTheme },
2527
+ // Adopt is the one door that moves the production pointer; naming a theme
2528
+ // here would be a second, silent way to publish.
2529
+ { method: "PUT", pattern: THEMES_PRODUCTION_ROUTE, handler: methodNotAllowed },
2530
+ { method: "POST", pattern: THEMES_PRODUCTION_ROUTE, handler: methodNotAllowed },
2531
+ { method: "DELETE", pattern: THEMES_PRODUCTION_ROUTE, handler: methodNotAllowed },
2532
+ // Themes — exact import route runs before :name regexes
2533
+ { method: "POST", pattern: THEME_IMPORT_ROUTE, handler: handleImportTheme },
2534
+ { method: "PUT", pattern: THEME_IMPORT_ROUTE, handler: methodNotAllowed },
2535
+ { method: "GET", pattern: THEME_IMPORT_ROUTE, handler: methodNotAllowed },
2536
+ { method: "DELETE", pattern: THEME_IMPORT_ROUTE, handler: methodNotAllowed },
2537
+ // Themes — :name/apply (more specific than :name)
2538
+ { method: "PUT", pattern: THEME_APPLY_REGEX, handler: handleApplyTheme },
2539
+ { method: "POST", pattern: THEME_APPLY_REGEX, handler: methodNotAllowed },
2540
+ { method: "GET", pattern: THEME_APPLY_REGEX, handler: methodNotAllowed },
2541
+ { method: "DELETE", pattern: THEME_APPLY_REGEX, handler: methodNotAllowed },
2542
+ // Themes — :name/export (more specific than :name)
2543
+ { method: "GET", pattern: THEME_EXPORT_REGEX, handler: handleExportTheme },
2544
+ { method: "PUT", pattern: THEME_EXPORT_REGEX, handler: methodNotAllowed },
2545
+ { method: "POST", pattern: THEME_EXPORT_REGEX, handler: methodNotAllowed },
2546
+ { method: "DELETE", pattern: THEME_EXPORT_REGEX, handler: methodNotAllowed },
2184
2547
  // Themes — :name CRUD (broadest theme route, runs last)
2185
2548
  { method: "GET", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
2186
2549
  { method: "PUT", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
2187
2550
  { method: "DELETE", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
2188
- // Manifests — list / active are exact strings, must run before regexes
2189
- { method: "GET", pattern: MANIFESTS_ROUTE, handler: handleListManifests },
2190
- { method: "GET", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleGetActiveManifest },
2191
- { method: "PUT", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleSetActiveManifest },
2192
- // Manifests — exact import route runs before :name regexes
2193
- { method: "POST", pattern: MANIFEST_IMPORT_ROUTE, handler: handleImportManifest },
2194
- { method: "PUT", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
2195
- { method: "GET", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
2196
- { method: "DELETE", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
2197
- // Manifests — :name/apply (more specific than :name)
2198
- { method: "PUT", pattern: MANIFEST_APPLY_REGEX, handler: handleApplyManifest },
2199
- { method: "POST", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
2200
- { method: "GET", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
2201
- { method: "DELETE", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
2202
- // Manifests — :name/export (more specific than :name)
2203
- { method: "GET", pattern: MANIFEST_EXPORT_REGEX, handler: handleExportManifest },
2204
- { method: "PUT", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
2205
- { method: "POST", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
2206
- { method: "DELETE", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
2207
- // Manifests — :name CRUD (broadest manifest route, runs last)
2208
- { method: "GET", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
2209
- { method: "PUT", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
2210
- { method: "DELETE", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName }
2551
+ // Production — the whole-look adopt door
2552
+ { method: "PUT", pattern: PRODUCTION_ROUTE, handler: handleAdoptLook },
2553
+ { method: "GET", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
2554
+ { method: "POST", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed },
2555
+ { method: "DELETE", pattern: PRODUCTION_ROUTE, handler: methodNotAllowed }
2211
2556
  ];
2212
2557
  return {
2213
2558
  name: "theme-file-api",
@@ -2221,19 +2566,42 @@ ${lines.join("\n")}
2221
2566
  };
2222
2567
  },
2223
2568
  configureServer(server) {
2224
- ensureThemesDir();
2225
- ensureComponentConfigsDir();
2226
- ensureManifestsDir();
2227
- regenerateTokensCss();
2228
- if (opts.autoMigrate) autoMigrateAdditive((msg) => server.config.logger.info(msg));
2229
- warnOnTokenDrift((msg) => server.config.logger.warn(msg));
2569
+ legacyLayout = detectLegacyLayout({
2570
+ dataDir: dataDirs.dataDir,
2571
+ colorsAndTypeDir: COLORS_AND_TYPE_DIR,
2572
+ themesDir: THEMES_DIR,
2573
+ configuredManifestsDir: dataDirs.legacyManifestsDir
2574
+ });
2575
+ if (legacyLayout) {
2576
+ warnOnLegacyLayout(legacyLayout.evidence, (msg) => server.config.logger.warn(msg));
2577
+ } else {
2578
+ ensureColorsAndTypeDir();
2579
+ ensureComponentConfigsDir();
2580
+ const { productionPointerHeld } = ensureThemesDir((msg) => server.config.logger.warn(msg));
2581
+ if (!productionPointerHeld || !import_fs4.default.existsSync(GENERATED_CSS_PATH)) regenerateTokensCss();
2582
+ if (opts.autoMigrate) autoMigrateAdditive((msg) => server.config.logger.info(msg));
2583
+ warnOnTokenDrift((msg) => server.config.logger.warn(msg));
2584
+ warnOnLegacyPointers(
2585
+ listLegacyPointers(),
2586
+ productionPointerHeld,
2587
+ (msg) => server.config.logger.warn(msg)
2588
+ );
2589
+ }
2230
2590
  server.middlewares.use(async (req, res, next) => {
2591
+ if (legacyLayout && req.method !== "GET" && (req.url ?? "").startsWith(API_BASE)) {
2592
+ jsonResponse(res, 409, {
2593
+ error: "This project still uses the data layout from 0.47 and earlier. Run `npx live-tokens migrate`, then restart the dev server.",
2594
+ code: "LEGACY_LAYOUT"
2595
+ });
2596
+ return;
2597
+ }
2231
2598
  const handled = await dispatch(req, res, routes);
2232
2599
  if (!handled) next();
2233
2600
  });
2234
2601
  },
2235
2602
  handleHotUpdate(ctx) {
2236
- const normalized = import_path3.default.resolve(ctx.file);
2603
+ if (legacyLayout) return;
2604
+ const normalized = import_path4.default.resolve(ctx.file);
2237
2605
  if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
2238
2606
  if (!normalized.endsWith(".svelte")) return;
2239
2607
  if (!isThemeAwareComponent(normalized)) return;