@motion-proto/live-tokens 0.47.1 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/.claude/skills/live-tokens-adjust-shape-space/SKILL.md +68 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-generate-theme/SKILL.md +152 -0
  4. package/CHANGELOG.md +270 -0
  5. package/README.md +37 -26
  6. package/bin/adjust.mjs +254 -0
  7. package/bin/cli.mjs +94 -7
  8. package/bin/generate-theme.mjs +251 -0
  9. package/bin/migrate.mjs +95 -6
  10. package/dist-plugin/adjust/index.cjs +259 -0
  11. package/dist-plugin/adjust/index.d.cts +45 -0
  12. package/dist-plugin/adjust/index.d.ts +45 -0
  13. package/dist-plugin/adjust/index.js +176 -0
  14. package/dist-plugin/{chunk-H4TRUINI.js → chunk-44RSTAII.js} +0 -48
  15. package/dist-plugin/chunk-6OZFXIQI.js +316 -0
  16. package/dist-plugin/chunk-76TFDTJO.js +556 -0
  17. package/dist-plugin/chunk-D3ZVKOR4.js +52 -0
  18. package/dist-plugin/dataPaths-DBN0RPuT.d.cts +54 -0
  19. package/dist-plugin/dataPaths-DBN0RPuT.d.ts +54 -0
  20. package/dist-plugin/generateColorsAndType/index.cjs +1781 -0
  21. package/dist-plugin/generateColorsAndType/index.d.cts +81 -0
  22. package/dist-plugin/generateColorsAndType/index.d.ts +81 -0
  23. package/dist-plugin/generateColorsAndType/index.js +1210 -0
  24. package/dist-plugin/index.cjs +849 -531
  25. package/dist-plugin/index.d.cts +3 -2
  26. package/dist-plugin/index.d.ts +3 -2
  27. package/dist-plugin/index.js +658 -1132
  28. package/dist-plugin/migrateData/index.cjs +728 -0
  29. package/dist-plugin/migrateData/index.d.cts +60 -0
  30. package/dist-plugin/migrateData/index.d.ts +60 -0
  31. package/dist-plugin/migrateData/index.js +348 -0
  32. package/dist-plugin/themeTypes-DMHZOnUn.d.cts +211 -0
  33. package/dist-plugin/themeTypes-DMHZOnUn.d.ts +211 -0
  34. package/dist-plugin/tokensCssMigrations/index.cjs +2 -1
  35. package/dist-plugin/tokensCssMigrations/index.d.cts +3 -15
  36. package/dist-plugin/tokensCssMigrations/index.d.ts +3 -15
  37. package/dist-plugin/tokensCssMigrations/index.js +4 -2
  38. package/package.json +18 -4
  39. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +183 -158
  40. package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +0 -4
  41. package/src/editor/component-editor/scaffolding/SaveAsDialog.svelte +4 -4
  42. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +2 -59
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +1 -1
  44. package/src/editor/core/components/adjustAliases.ts +180 -0
  45. package/src/editor/core/components/aliasKinds.ts +73 -0
  46. package/src/editor/core/components/componentConfigService.ts +26 -38
  47. package/src/editor/core/flashStatus.ts +1 -1
  48. package/src/editor/core/fonts/fontMigration.ts +13 -13
  49. package/src/editor/core/fonts/fontPairing.ts +35 -0
  50. package/src/editor/core/palettes/paletteDerivation.ts +83 -13
  51. package/src/editor/core/preview/lookPreview.ts +141 -0
  52. package/src/editor/core/productionPulse.ts +34 -21
  53. package/src/editor/core/storage/files/versionedFileResourceClient.ts +18 -45
  54. package/src/editor/core/store/editorConfigStore.ts +2 -2
  55. package/src/editor/core/store/editorPersistence.ts +0 -1
  56. package/src/editor/core/store/editorStore.ts +114 -51
  57. package/src/editor/core/store/editorTypes.ts +0 -1
  58. package/src/editor/core/store/gradientSource.ts +2 -2
  59. package/src/editor/core/themes/colorsAndTypeService.ts +95 -0
  60. package/src/editor/core/themes/generateColorsAndType.ts +433 -0
  61. package/src/editor/core/themes/loadRows.ts +64 -0
  62. package/src/editor/core/themes/lookSummary.ts +75 -0
  63. package/src/editor/core/themes/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +3 -3
  64. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -2
  65. package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +3 -3
  66. package/src/editor/core/themes/migrations/2026-08-13-drop-legacy-shape-space-keys.ts +38 -0
  67. package/src/editor/core/themes/migrations/2026-08-15-line-height-scale-rename.ts +57 -0
  68. package/src/editor/core/themes/migrations/index.ts +19 -11
  69. package/src/editor/core/themes/slices/components.ts +4 -4
  70. package/src/editor/core/themes/slices/fonts.ts +1 -1
  71. package/src/editor/core/themes/slices/gradients.ts +22 -0
  72. package/src/editor/core/themes/slices/palettes.ts +2 -2
  73. package/src/editor/core/themes/themeInit.ts +21 -29
  74. package/src/editor/core/themes/themeService.ts +218 -78
  75. package/src/editor/core/themes/themeTypes.ts +94 -51
  76. package/src/editor/docs/Docs.svelte +1 -0
  77. package/src/editor/docs/chapters.ts +1 -0
  78. package/src/editor/docs/content/01-overview.md +1 -1
  79. package/src/editor/docs/content/editing-tokens.md +6 -6
  80. package/src/editor/docs/content/getting-started.md +9 -7
  81. package/src/editor/docs/content/themes-workflow.md +74 -34
  82. package/src/editor/docs/content/where-themes-live.md +61 -0
  83. package/src/editor/docs/content.generated.ts +5 -4
  84. package/src/editor/index.ts +27 -27
  85. package/src/editor/pages/ComponentEditorPage.svelte +2 -2
  86. package/src/editor/pages/EditorShell.svelte +4 -30
  87. package/src/editor/ui/FileLoadList.svelte +67 -7
  88. package/src/editor/ui/GradientEditor.svelte +3 -3
  89. package/src/editor/ui/ThemePanel.svelte +1067 -0
  90. package/src/editor/ui/UIDialog.svelte +11 -9
  91. package/src/editor/ui/index.ts +4 -2
  92. package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
  93. package/src/live-tokens/data/{themes → colors-and-type}/default.json +962 -549
  94. package/src/live-tokens/data/colors-and-type/halloween.json +2529 -0
  95. package/src/live-tokens/data/colors-and-type/midnight-study.json +2536 -0
  96. package/src/live-tokens/data/colors-and-type/ocean.json +2489 -0
  97. package/src/live-tokens/data/colors-and-type/royal-velvet.json +2520 -0
  98. package/src/live-tokens/data/colors-and-type/spring-meadow.json +2476 -0
  99. package/src/live-tokens/data/colors-and-type/sunset.json +2528 -0
  100. package/src/live-tokens/data/themes/autumn.json +3918 -0
  101. package/src/live-tokens/data/themes/halloween.json +3992 -0
  102. package/src/live-tokens/data/themes/midnight-study.json +3932 -0
  103. package/src/live-tokens/data/themes/ocean.json +3917 -0
  104. package/src/live-tokens/data/themes/royal-velvet.json +3983 -0
  105. package/src/live-tokens/data/themes/spring-meadow.json +3904 -0
  106. package/src/live-tokens/data/themes/sunset.json +3931 -0
  107. package/src/live-tokens/data/tokens.generated.css +129 -140
  108. package/src/system/styles/CONVENTIONS.md +1 -1
  109. package/src/system/styles/fonts.css +1 -1
  110. package/template/README.md +7 -5
  111. package/template/_gitignore +0 -6
  112. package/src/editor/core/components/componentPersist.ts +0 -62
  113. package/src/editor/core/manifests/manifestService.ts +0 -172
  114. package/src/editor/ui/ManifestFileManager.svelte +0 -446
  115. package/src/editor/ui/ThemeFileManager.svelte +0 -785
  116. package/src/live-tokens/data/manifests/default.json +0 -35
@@ -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
@@ -349,7 +349,33 @@ function nearestPaletteStep(lightnessCurve, baseL) {
349
349
  }
350
350
  return best;
351
351
  }
352
- 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
+ }
353
379
  const existing = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
354
380
  if (existing >= 0) {
355
381
  const displacedY = curve[existing].y;
@@ -382,7 +408,8 @@ function fitBetween(a, prev, next) {
382
408
  }
383
409
  return out;
384
410
  }
385
- function liftCurveAnchor(curve, x, displacedY) {
411
+ function liftCurveAnchor(curve, x, displacedY, priorEndpoints) {
412
+ if (priorEndpoints) return priorEndpoints;
386
413
  const idx = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
387
414
  if (idx < 0) return curve;
388
415
  if (displacedY !== void 0) return curve.map((a, i) => i === idx ? { ...a, y: displacedY } : a);
@@ -400,6 +427,7 @@ function liftCurveAnchor(curve, x, displacedY) {
400
427
  function syncBaseAnchor(cfg) {
401
428
  if (cfg.anchorToBase === false) return;
402
429
  const prev = cfg.anchorPlacement;
430
+ const fresh = prev === void 0;
403
431
  let lCurve = cfg.lightnessCurve;
404
432
  let sCurve = cfg.saturationCurve;
405
433
  if (prev) {
@@ -409,11 +437,17 @@ function syncBaseAnchor(cfg) {
409
437
  }
410
438
  const step = nearestPaletteStep(lCurve, cfg.baseColor.l);
411
439
  const x = stepIndexToX(step);
412
- const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100);
413
- 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);
414
442
  cfg.lightnessCurve = l.curve;
415
443
  cfg.saturationCurve = s.curve;
416
- 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
+ };
417
451
  }
418
452
  function computeDerivedOklch(step, base, scaleTitle, scaleCurves, curveOffset) {
419
453
  const scale = SCALES.find((s) => s.title === scaleTitle);
@@ -590,6 +624,7 @@ function versionedFileResourceServer(opts) {
590
624
  const dir = opts.dir;
591
625
  const activePath = import_path.default.join(dir, "_active.json");
592
626
  const productionPath = import_path.default.join(dir, "_production.json");
627
+ const workingPath = import_path.default.join(dir, "_working.json");
593
628
  const defaultName = opts.defaultName ?? "default";
594
629
  const resolvedDir = import_path.default.resolve(dir);
595
630
  const resolvedPackageDir = opts.packageDir ? import_path.default.resolve(opts.packageDir) : null;
@@ -618,6 +653,16 @@ function versionedFileResourceServer(opts) {
618
653
  }
619
654
  return null;
620
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
+ }
621
666
  function readJson(name) {
622
667
  const resolved = existingPath(name);
623
668
  if (!resolved) return null;
@@ -662,20 +707,44 @@ function versionedFileResourceServer(opts) {
662
707
  function setProductionName(name) {
663
708
  import_fs.default.writeFileSync(productionPath, JSON.stringify({ productionFile: name }));
664
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
+ }
665
728
  return {
666
729
  dir,
667
730
  activePath,
668
731
  productionPath,
732
+ workingPath,
669
733
  ensureDir,
670
734
  ensureMeta,
671
735
  filePath,
672
736
  existingPath,
737
+ isPackageFile,
673
738
  readJson,
674
739
  listNames,
675
740
  getActiveName,
676
741
  getProductionName,
677
742
  setActiveName,
678
- setProductionName
743
+ setProductionName,
744
+ hasWorking,
745
+ readWorking,
746
+ writeWorking,
747
+ clearWorking
679
748
  };
680
749
  }
681
750
 
@@ -707,6 +776,78 @@ async function dispatch(req, res, routes) {
707
776
  return false;
708
777
  }
709
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
+
710
851
  // vite-plugin/files/nameAllocator.ts
711
852
  function nextAvailableName(exists, baseName, maxAttempts = 1e3) {
712
853
  if (!exists(baseName)) return baseName;
@@ -723,8 +864,9 @@ var import_path2 = __toESM(require("path"), 1);
723
864
  var DEFAULT_DATA_DIR = "src/live-tokens/data";
724
865
  var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
725
866
  "dataDir",
726
- "themesDir",
867
+ "colorsAndTypeDir",
727
868
  "componentConfigsDir",
869
+ "themesDir",
728
870
  "manifestsDir",
729
871
  "tokensCssPath"
730
872
  ]);
@@ -757,12 +899,69 @@ function resolveDataDirs(opts = {}) {
757
899
  const sub = (name) => import_path2.default.resolve(dataDir, name);
758
900
  return {
759
901
  dataDir,
760
- 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"),
761
903
  componentConfigsDir: opts.componentConfigsDir ? import_path2.default.resolve(opts.componentConfigsDir) : fileConfig.componentConfigsDir ? import_path2.default.resolve(fileConfig.componentConfigsDir) : sub("component-configs"),
762
- 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
763
906
  };
764
907
  }
765
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
+
766
965
  // vite-plugin/tokensCssMigrations/cssTokenOps.ts
767
966
  var DECL_RE = /(^|[\s;{])(--[a-z0-9-]+)\s*:/gi;
768
967
  var REF_RE = /var\(\s*(--[a-z0-9-]+)/gi;
@@ -1163,14 +1362,14 @@ var import_node_url = require("url");
1163
1362
  var import_meta = {};
1164
1363
  var PKG_VERSION = (() => {
1165
1364
  try {
1166
- 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));
1167
1366
  for (let i = 0; i < 4; i++) {
1168
- const p = import_path3.default.join(dir, "package.json");
1169
- if (import_fs3.default.existsSync(p)) {
1170
- 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"));
1171
1370
  if (json?.name === "@motion-proto/live-tokens") return json.version ?? "";
1172
1371
  }
1173
- const up = import_path3.default.dirname(dir);
1372
+ const up = import_path4.default.dirname(dir);
1174
1373
  if (up === dir) break;
1175
1374
  dir = up;
1176
1375
  }
@@ -1178,65 +1377,79 @@ var PKG_VERSION = (() => {
1178
1377
  }
1179
1378
  return "";
1180
1379
  })();
1380
+ var BUNDLE_KIND_BY_VERSION = {
1381
+ 1: "manifest-bundle",
1382
+ [THEME_SCHEMA_VERSION]: "theme-bundle"
1383
+ };
1181
1384
  function themeFileApi(opts) {
1182
1385
  const dataDirs = resolveDataDirs({
1183
1386
  dataDir: opts.dataDir,
1184
- themesDir: opts.themesDir,
1387
+ colorsAndTypeDir: opts.colorsAndTypeDir,
1185
1388
  componentConfigsDir: opts.componentConfigsDir,
1186
- manifestsDir: opts.manifestsDir
1389
+ themesDir: opts.themesDir
1187
1390
  });
1188
- const THEMES_DIR = dataDirs.themesDir;
1391
+ const COLORS_AND_TYPE_DIR = dataDirs.colorsAndTypeDir;
1189
1392
  const COMPONENT_CONFIGS_DIR = dataDirs.componentConfigsDir;
1190
- const MANIFESTS_DIR = dataDirs.manifestsDir;
1191
- const CSS_PATH = import_path3.default.resolve(opts.tokensCssPath);
1192
- const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? import_path3.default.resolve(opts.tokensGeneratedCssPath) : import_path3.default.join(dataDirs.dataDir, "tokens.generated.css");
1193
- 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");
1194
1397
  const API_BASE = opts.apiBase ?? "/api/live-tokens";
1195
- const consumerComponentDirs = opts.componentsSrcDir ? [import_path3.default.resolve(opts.componentsSrcDir)] : [import_path3.default.resolve("src/components"), import_path3.default.resolve("src/system/components")];
1196
- const packageComponentsDir = import_path3.default.resolve(
1197
- 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)),
1198
1401
  "..",
1199
1402
  "src",
1200
1403
  "system",
1201
1404
  "components"
1202
1405
  );
1203
- const packageDataDir = import_path3.default.resolve(
1204
- 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)),
1205
1408
  "..",
1206
1409
  "src",
1207
1410
  "live-tokens",
1208
1411
  "data"
1209
1412
  );
1210
- const packageThemesDir = import_path3.default.join(packageDataDir, "themes");
1211
- const packageManifestsDir = import_path3.default.join(packageDataDir, "manifests");
1212
- 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
+ }
1213
1426
  const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
1214
- if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs3.default.existsSync(packageComponentsDir)) {
1427
+ if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs4.default.existsSync(packageComponentsDir)) {
1215
1428
  COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
1216
1429
  }
1217
- const themesResource = versionedFileResourceServer({
1218
- dir: THEMES_DIR,
1219
- packageDir: packageThemesDir
1430
+ const colorsAndTypeResource = versionedFileResourceServer({
1431
+ dir: COLORS_AND_TYPE_DIR,
1432
+ packageDir: packageColorsAndTypeDir,
1433
+ packageOwnedNames: shippedColorsAndTypeNames(packageDataDir)
1220
1434
  });
1221
1435
  const componentResourceCache = /* @__PURE__ */ new Map();
1222
1436
  function componentResource(comp) {
1223
1437
  let r = componentResourceCache.get(comp);
1224
1438
  if (!r) {
1225
1439
  r = versionedFileResourceServer({
1226
- dir: import_path3.default.join(COMPONENT_CONFIGS_DIR, comp),
1227
- 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)
1228
1442
  });
1229
1443
  componentResourceCache.set(comp, r);
1230
1444
  }
1231
1445
  return r;
1232
1446
  }
1233
- const manifestsResource = versionedFileResourceServer({
1234
- dir: MANIFESTS_DIR,
1235
- packageDir: packageManifestsDir
1447
+ const themesResource = versionedFileResourceServer({
1448
+ dir: THEMES_DIR,
1449
+ packageDir: packageThemesDir
1236
1450
  });
1237
- function ensureThemesDir() {
1238
- themesResource.ensureDir();
1239
- themesResource.ensureMeta();
1451
+ function ensureColorsAndTypeDir() {
1452
+ colorsAndTypeResource.ensureDir();
1240
1453
  }
1241
1454
  function readBody(req) {
1242
1455
  return new Promise((resolve, reject) => {
@@ -1251,25 +1464,33 @@ function themeFileApi(opts) {
1251
1464
  res.setHeader("Content-Type", "application/json");
1252
1465
  res.end(JSON.stringify(data));
1253
1466
  }
1254
- function normalizeTheme(theme) {
1255
- if (!theme || typeof theme !== "object") return theme;
1256
- const palettes = theme.editorConfigs ?? {};
1257
- 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 ?? {};
1258
1479
  const { palettes: nextPalettes, consumed } = reconcilePalettesFromCssVars(palettes, cssVars);
1259
1480
  const nextCssVars = {};
1260
1481
  for (const [k, v] of Object.entries(cssVars)) {
1261
1482
  if (!consumed.has(k)) nextCssVars[k] = v;
1262
1483
  }
1263
- return { ...theme, editorConfigs: nextPalettes, cssVariables: nextCssVars };
1484
+ return { ...colorsAndType, editorConfigs: nextPalettes, cssVariables: nextCssVars };
1264
1485
  }
1265
1486
  const SYSTEM_CASCADES_SSR = {
1266
1487
  "system-ui-sans": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
1267
1488
  "system-ui-serif": 'Georgia, "Times New Roman", serif',
1268
1489
  "system-ui-mono": 'ui-monospace, "SF Mono", Menlo, Consolas, monospace'
1269
1490
  };
1270
- function resolveFontStacks(themeData) {
1271
- const stacks = themeData.fontStacks;
1272
- const sources = themeData.fontSources;
1491
+ function resolveFontStacks(colorsAndTypeData) {
1492
+ const stacks = colorsAndTypeData.fontStacks;
1493
+ const sources = colorsAndTypeData.fontSources;
1273
1494
  if (!stacks || stacks.length === 0) return {};
1274
1495
  const familyById = /* @__PURE__ */ new Map();
1275
1496
  for (const src of sources ?? []) {
@@ -1295,21 +1516,28 @@ function themeFileApi(opts) {
1295
1516
  }
1296
1517
  function regenerateTokensCss() {
1297
1518
  const lines = [];
1298
- 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. */");
1299
1520
  lines.push("/* tokens.css holds developer-authored defaults; this file holds editor overrides. */");
1300
1521
  lines.push("");
1301
1522
  const productionThemeName = themesResource.getProductionName();
1302
- const themeData = themesResource.readJson(productionThemeName);
1303
- let themeVarCount = 0;
1304
- if (themeData) {
1305
- const cssVars = { ...themeData.cssVariables || {} };
1306
- Object.assign(cssVars, palettesToVars(migratePaletteColorsToOklch(themeData.editorConfigs ?? {})));
1307
- 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);
1308
1536
  for (const [name, value] of Object.entries(resolvedFontVars)) {
1309
1537
  cssVars[name] = value;
1310
1538
  }
1311
- themeVarCount = Object.keys(cssVars).length;
1312
- if (themeVarCount > 0) {
1539
+ colorsAndTypeVarCount = Object.keys(cssVars).length;
1540
+ if (colorsAndTypeVarCount > 0) {
1313
1541
  lines.push(`/* Production theme: ${productionThemeName} */`);
1314
1542
  lines.push(":root:root {");
1315
1543
  for (const [name, value] of Object.entries(cssVars)) {
@@ -1320,12 +1548,10 @@ function themeFileApi(opts) {
1320
1548
  }
1321
1549
  }
1322
1550
  let componentOverrideCount = 0;
1323
- if (import_fs3.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1551
+ if (import_fs4.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1324
1552
  const blocks = [];
1325
1553
  for (const comp of listComponentNames()) {
1326
- const prod = componentResource(comp).getProductionName();
1327
- if (prod === "default") continue;
1328
- const prodCfg = readComponentConfig(comp, prod);
1554
+ const prodCfg = productionTheme?.componentConfigs[comp];
1329
1555
  const defaultCfg = readComponentConfig(comp, "default");
1330
1556
  if (!prodCfg || !defaultCfg) continue;
1331
1557
  const overrides = [];
@@ -1336,7 +1562,7 @@ function themeFileApi(opts) {
1336
1562
  }
1337
1563
  }
1338
1564
  if (overrides.length === 0) continue;
1339
- const block = [` /* ${comp} (${prod}) */`];
1565
+ const block = [` /* ${comp} */`];
1340
1566
  for (const [varName, semanticValue] of overrides) {
1341
1567
  block.push(` ${varName}: ${aliasValueToCss(semanticValue)};`);
1342
1568
  }
@@ -1354,24 +1580,20 @@ function themeFileApi(opts) {
1354
1580
  lines.push("");
1355
1581
  }
1356
1582
  }
1357
- if (!import_fs3.default.existsSync(import_path3.default.dirname(GENERATED_CSS_PATH))) {
1358
- 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 });
1359
1585
  }
1360
- import_fs3.default.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
1586
+ import_fs4.default.writeFileSync(GENERATED_CSS_PATH, lines.join("\n"));
1361
1587
  console.log(
1362
- `[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)`
1363
1589
  );
1364
1590
  }
1365
- function syncTokensToCss(_fileName) {
1366
- regenerateTokensCss();
1367
- }
1368
- function syncFontsToCss(fileName) {
1369
- const themeData = themesResource.readJson(fileName);
1370
- if (!themeData) return;
1371
- const sources = themeData.fontSources;
1591
+ function syncFontsToCss(colorsAndTypeData) {
1592
+ if (!colorsAndTypeData) return;
1593
+ const sources = colorsAndTypeData.fontSources;
1372
1594
  if (!sources) return;
1373
1595
  const lines = [];
1374
- 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. */");
1375
1597
  lines.push("/* Both fonts.css and fonts/ are in dist/, so relative paths work at runtime. */");
1376
1598
  lines.push("");
1377
1599
  const urlSources = sources.filter((s) => s.kind !== "font-face" && s.url);
@@ -1391,11 +1613,11 @@ function themeFileApi(opts) {
1391
1613
  lines.push("");
1392
1614
  }
1393
1615
  const content = lines.join("\n");
1394
- if (!import_fs3.default.existsSync(import_path3.default.dirname(FONTS_CSS_PATH))) {
1395
- 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 });
1396
1618
  }
1397
- import_fs3.default.writeFileSync(FONTS_CSS_PATH, content);
1398
- 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)}`);
1399
1621
  }
1400
1622
  function extractAliasDeclarations(body) {
1401
1623
  const aliases = {};
@@ -1410,11 +1632,11 @@ function themeFileApi(opts) {
1410
1632
  return aliases;
1411
1633
  }
1412
1634
  function componentNameFromFile(filePath) {
1413
- return import_path3.default.basename(filePath, ".svelte").toLowerCase();
1635
+ return import_path4.default.basename(filePath, ".svelte").toLowerCase();
1414
1636
  }
1415
1637
  function isThemeAwareComponent(filePath) {
1416
1638
  try {
1417
- const source = import_fs3.default.readFileSync(filePath, "utf-8");
1639
+ const source = import_fs4.default.readFileSync(filePath, "utf-8");
1418
1640
  return /:global\(:root\)\s*\{/.test(source);
1419
1641
  } catch {
1420
1642
  return false;
@@ -1423,10 +1645,10 @@ function themeFileApi(opts) {
1423
1645
  function listComponentSourcePaths() {
1424
1646
  const byName = /* @__PURE__ */ new Map();
1425
1647
  for (const dir of COMPONENTS_SCAN_DIRS) {
1426
- if (!import_fs3.default.existsSync(dir)) continue;
1427
- 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)) {
1428
1650
  if (!f.endsWith(".svelte")) continue;
1429
- const full = import_path3.default.join(dir, f);
1651
+ const full = import_path4.default.join(dir, f);
1430
1652
  if (!isThemeAwareComponent(full)) continue;
1431
1653
  const name = componentNameFromFile(f);
1432
1654
  if (!byName.has(name)) byName.set(name, full);
@@ -1440,18 +1662,18 @@ function themeFileApi(opts) {
1440
1662
  function warnOnTokenDrift(log) {
1441
1663
  let tokensCss = "";
1442
1664
  try {
1443
- tokensCss = import_fs3.default.readFileSync(CSS_PATH, "utf-8");
1665
+ tokensCss = import_fs4.default.readFileSync(CSS_PATH, "utf-8");
1444
1666
  } catch {
1445
1667
  return;
1446
1668
  }
1447
1669
  let generatedCss = "";
1448
1670
  try {
1449
- generatedCss = import_fs3.default.readFileSync(GENERATED_CSS_PATH, "utf-8");
1671
+ generatedCss = import_fs4.default.readFileSync(GENERATED_CSS_PATH, "utf-8");
1450
1672
  } catch {
1451
1673
  }
1452
1674
  const componentSources = listComponentSourcePaths().map((p) => ({
1453
1675
  name: componentNameFromFile(p),
1454
- source: import_fs3.default.readFileSync(p, "utf-8")
1676
+ source: import_fs4.default.readFileSync(p, "utf-8")
1455
1677
  }));
1456
1678
  const missing = validateTokensCss({ tokensCss, generatedCss, componentSources });
1457
1679
  if (missing.length === 0) return;
@@ -1459,7 +1681,7 @@ function themeFileApi(opts) {
1459
1681
  (m) => ` ${m.token} (referenced by ${m.referencedBy.join(", ")})`
1460
1682
  );
1461
1683
  log(
1462
- `[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)}:
1463
1685
  ${lines.join("\n")}
1464
1686
  These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
1465
1687
  );
@@ -1467,37 +1689,37 @@ ${lines.join("\n")}
1467
1689
  function autoMigrateAdditive(log) {
1468
1690
  let before = "";
1469
1691
  try {
1470
- before = import_fs3.default.readFileSync(CSS_PATH, "utf-8");
1692
+ before = import_fs4.default.readFileSync(CSS_PATH, "utf-8");
1471
1693
  } catch {
1472
1694
  return;
1473
1695
  }
1474
1696
  const { css, applied, changed } = runAdditiveTokensCssMigrations(before);
1475
1697
  if (!changed) return;
1476
- import_fs3.default.writeFileSync(CSS_PATH, css);
1698
+ import_fs4.default.writeFileSync(CSS_PATH, css);
1477
1699
  log(
1478
- `[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.`
1479
1701
  );
1480
1702
  }
1481
1703
  function generateDefaultConfig(comp, sourcePath) {
1482
- if (!import_fs3.default.existsSync(sourcePath)) return;
1704
+ if (!import_fs4.default.existsSync(sourcePath)) return;
1483
1705
  const r = componentResource(comp);
1484
1706
  r.ensureDir();
1485
1707
  const defaultPath = r.filePath("default");
1486
- const sourceStat = import_fs3.default.statSync(sourcePath);
1487
- if (import_fs3.default.existsSync(defaultPath)) {
1488
- 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);
1489
1711
  if (defaultStat.mtimeMs >= sourceStat.mtimeMs) return;
1490
1712
  }
1491
- const source = import_fs3.default.readFileSync(sourcePath, "utf-8");
1713
+ const source = import_fs4.default.readFileSync(sourcePath, "utf-8");
1492
1714
  const body = extractGlobalRootBody(source);
1493
1715
  const aliases = extractAliasDeclarations(body);
1494
1716
  const now = (/* @__PURE__ */ new Date()).toISOString();
1495
1717
  let createdAt = now;
1496
1718
  let existingUpdatedAt;
1497
1719
  let existingAliases;
1498
- if (import_fs3.default.existsSync(defaultPath)) {
1720
+ if (import_fs4.default.existsSync(defaultPath)) {
1499
1721
  try {
1500
- const existing = JSON.parse(import_fs3.default.readFileSync(defaultPath, "utf-8"));
1722
+ const existing = JSON.parse(import_fs4.default.readFileSync(defaultPath, "utf-8"));
1501
1723
  if (existing.createdAt) createdAt = existing.createdAt;
1502
1724
  if (typeof existing.updatedAt === "string") existingUpdatedAt = existing.updatedAt;
1503
1725
  if (existing.aliases && typeof existing.aliases === "object") {
@@ -1523,57 +1745,191 @@ ${lines.join("\n")}
1523
1745
  };
1524
1746
  if (aliasesUnchanged && existingUpdatedAt) {
1525
1747
  const t = /* @__PURE__ */ new Date();
1526
- import_fs3.default.utimesSync(defaultPath, t, t);
1748
+ import_fs4.default.utimesSync(defaultPath, t, t);
1527
1749
  return;
1528
1750
  }
1529
- import_fs3.default.writeFileSync(defaultPath, JSON.stringify(defaultConfig, null, 2));
1751
+ import_fs4.default.writeFileSync(defaultPath, JSON.stringify(defaultConfig, null, 2));
1530
1752
  }
1531
1753
  function ensureComponentConfigsDir() {
1532
- if (!import_fs3.default.existsSync(COMPONENT_CONFIGS_DIR)) {
1533
- 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 });
1534
1756
  }
1535
1757
  for (const sourcePath of listComponentSourcePaths()) {
1536
1758
  const comp = componentNameFromFile(sourcePath);
1537
1759
  const r = componentResource(comp);
1538
1760
  r.ensureDir();
1539
1761
  generateDefaultConfig(comp, sourcePath);
1540
- r.ensureMeta();
1541
1762
  }
1542
1763
  }
1543
- function ensureManifestsDir() {
1544
- manifestsResource.ensureDir();
1545
- if (!import_fs3.default.existsSync(manifestsResource.activePath)) {
1546
- import_fs3.default.writeFileSync(
1547
- manifestsResource.activePath,
1548
- JSON.stringify({ activeFile: "default" })
1549
- );
1550
- } else {
1551
- const activeName = manifestsResource.getActiveName();
1552
- if (manifestsResource.existingPath(activeName) === null) {
1553
- 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));
1554
1776
  }
1555
1777
  }
1778
+ return legacy;
1556
1779
  }
1557
- function patchActiveManifest(field, comp, fileName) {
1558
- const activeFile = manifestsResource.getActiveName();
1559
- if (activeFile === "default") return false;
1560
- const manifestPath = manifestsResource.filePath(activeFile);
1561
- if (!import_fs3.default.existsSync(manifestPath)) return false;
1562
- let manifest;
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
+ );
1790
+ }
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;
1563
1798
  try {
1564
- manifest = JSON.parse(import_fs3.default.readFileSync(manifestPath, "utf-8"));
1799
+ raw = themesResource.readJson(fileName);
1565
1800
  } catch {
1566
- return false;
1801
+ return null;
1567
1802
  }
1568
- if (field === "theme") {
1569
- manifest.theme = fileName;
1570
- } else if (field === "component" && comp) {
1571
- manifest.componentConfigs = manifest.componentConfigs ?? {};
1572
- manifest.componentConfigs[comp] = fileName;
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
+ }
1814
+ } catch {
1573
1815
  }
1574
- manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1575
- import_fs3.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1576
- return true;
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
+ }
1865
+ }
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" };
1577
1933
  }
1578
1934
  function formatAliasGradient(v) {
1579
1935
  const stopColor = (s) => {
@@ -1609,237 +1965,191 @@ ${lines.join("\n")}
1609
1965
  return null;
1610
1966
  }
1611
1967
  }
1612
- function syncComponentsToCss() {
1613
- regenerateTokensCss();
1614
- }
1615
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`;
1616
1974
  const THEMES_ROUTE = `${API_BASE}/themes`;
1617
1975
  const THEMES_ACTIVE_ROUTE = `${API_BASE}/themes/active`;
1618
1976
  const THEMES_PRODUCTION_ROUTE = `${API_BASE}/themes/production`;
1619
- const COMPONENT_CONFIGS_ROUTE = `${API_BASE}/component-configs`;
1620
- const MANIFESTS_ROUTE = `${API_BASE}/manifests`;
1621
- const MANIFESTS_ACTIVE_ROUTE = `${API_BASE}/manifests/active`;
1622
- 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\\-_]+)$`);
1623
1978
  const COMP_LIST_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)$`);
1624
1979
  const COMP_ACTIVE_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/active$`);
1625
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$`);
1626
1982
  const COMP_BY_NAME_REGEX = new RegExp(`^${escapedBase}/component-configs/([a-z0-9\\-_]+)/([a-z0-9\\-_]+)$`);
1627
- const MANIFEST_APPLY_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/apply$`);
1628
- const MANIFEST_EXPORT_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)/export$`);
1629
- const MANIFEST_IMPORT_ROUTE = `${API_BASE}/manifests/import`;
1630
- const MANIFEST_BY_NAME_REGEX = new RegExp(`^${escapedBase}/manifests/([a-z0-9\\-_]+)$`);
1631
- async function handleListThemes(_ctx) {
1632
- const activeFile = themesResource.getActiveName();
1633
- const files = themesResource.listNames().map((fileName) => {
1634
- 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);
1635
1991
  return {
1636
1992
  name: data?.name || fileName,
1637
1993
  fileName,
1638
1994
  updatedAt: data?.updatedAt || "",
1639
- 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)
1640
2000
  };
1641
2001
  });
1642
2002
  jsonResponse(_ctx.res, 200, { files });
1643
2003
  }
1644
- async function handleGetActiveTheme({ res }) {
1645
- const activeFile = themesResource.getActiveName();
1646
- const raw = themesResource.readJson(activeFile);
1647
- if (!raw) {
1648
- jsonResponse(res, 404, { error: "Active theme not found" });
1649
- return;
1650
- }
1651
- const data = normalizeTheme(raw);
1652
- data._fileName = activeFile;
1653
- jsonResponse(res, 200, data);
1654
- }
1655
- async function handleSetActiveTheme({ req, res }) {
1656
- const body = JSON.parse(await readBody(req));
1657
- const fileName = sanitizeFileName(body.name || "default");
1658
- if (themesResource.existingPath(fileName) === null) {
1659
- jsonResponse(res, 404, { error: "Theme not found" });
1660
- return;
1661
- }
1662
- themesResource.setActiveName(fileName);
1663
- jsonResponse(res, 200, { ok: true, activeFile: fileName });
1664
- }
1665
- async function handleGetProductionTheme({ res }) {
1666
- const prodFile = themesResource.getProductionName();
1667
- const raw = themesResource.readJson(prodFile);
1668
- if (!raw) {
1669
- 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" });
1670
2008
  return;
1671
2009
  }
1672
- const data = normalizeTheme(raw);
1673
2010
  jsonResponse(res, 200, {
1674
- fileName: prodFile,
1675
- name: data.name || prodFile,
1676
- updatedAt: data.updatedAt || "",
1677
- cssVariables: data.cssVariables || {}
2011
+ ...resolved.data,
2012
+ _fileName: themesResource.getActiveName(),
2013
+ _source: resolved.source
1678
2014
  });
1679
2015
  }
1680
- async function handleSetProductionTheme({ req, res }) {
1681
- const body = JSON.parse(await readBody(req));
1682
- const fileName = sanitizeFileName(body.name || "default");
1683
- if (themesResource.existingPath(fileName) === null) {
1684
- jsonResponse(res, 404, { error: "Theme not found" });
1685
- 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;
1686
2022
  }
1687
- if (manifestsResource.getActiveName() === "default") {
1688
- jsonResponse(res, 409, {
1689
- error: "Active manifest is protected. Save As first.",
1690
- code: "ACTIVE_IS_PROTECTED"
1691
- });
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" });
1692
2033
  return;
1693
2034
  }
1694
- themesResource.setProductionName(fileName);
1695
- syncTokensToCss(fileName);
1696
- syncFontsToCss(fileName);
1697
- syncComponentsToCss();
1698
- patchActiveManifest("theme", null, fileName);
1699
- const data = themesResource.readJson(fileName);
1700
- jsonResponse(res, 200, {
1701
- ok: true,
1702
- fileName,
1703
- name: data?.name || fileName,
1704
- updatedAt: data?.updatedAt || ""
1705
- });
2035
+ jsonResponse(res, 200, normalizeColorsAndType(raw));
1706
2036
  }
1707
- 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 }) {
1708
2048
  const [fileName] = params;
1709
- const filePath = themesResource.filePath(fileName);
2049
+ if (rejectReservedName(res, fileName)) return;
2050
+ const filePath = colorsAndTypeResource.filePath(fileName);
1710
2051
  if (req.method === "GET") {
1711
- const raw = themesResource.readJson(fileName);
2052
+ const raw = colorsAndTypeResource.readJson(fileName);
1712
2053
  if (!raw) {
1713
2054
  jsonResponse(res, 404, { error: "Not found" });
1714
2055
  return;
1715
2056
  }
1716
- const data = normalizeTheme(raw);
2057
+ const data = normalizeColorsAndType(raw);
1717
2058
  data._fileName = fileName;
1718
2059
  jsonResponse(res, 200, data);
1719
2060
  return;
1720
2061
  }
1721
2062
  if (req.method === "PUT") {
1722
2063
  if (fileName === "default") {
1723
- 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)" });
1724
2065
  return;
1725
2066
  }
1726
2067
  const body = JSON.parse(await readBody(req));
1727
2068
  body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1728
- if (import_fs3.default.existsSync(filePath)) {
2069
+ if (import_fs4.default.existsSync(filePath)) {
1729
2070
  try {
1730
- const existing = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
2071
+ const existing = JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
1731
2072
  if (existing.createdAt) body.createdAt = existing.createdAt;
1732
2073
  } catch {
1733
2074
  }
1734
2075
  }
1735
2076
  if (!body.createdAt) body.createdAt = body.updatedAt;
1736
- import_fs3.default.writeFileSync(filePath, JSON.stringify(body, null, 2));
1737
- if (fileName === themesResource.getProductionName()) {
1738
- syncTokensToCss(fileName);
1739
- syncFontsToCss(fileName);
1740
- syncComponentsToCss();
1741
- }
2077
+ import_fs4.default.writeFileSync(filePath, JSON.stringify(body, null, 2));
1742
2078
  jsonResponse(res, 200, { ok: true, fileName });
1743
2079
  return;
1744
2080
  }
1745
2081
  if (req.method === "DELETE") {
1746
2082
  if (fileName === "default") {
1747
- jsonResponse(res, 403, { error: "Cannot delete the default theme" });
2083
+ jsonResponse(res, 403, { error: "Cannot delete the default colors and type" });
1748
2084
  return;
1749
2085
  }
1750
- if (themesResource.getProductionName() === fileName) {
2086
+ if (import_fs4.default.existsSync(filePath)) {
2087
+ import_fs4.default.unlinkSync(filePath);
2088
+ } else if (colorsAndTypeResource.existingPath(fileName)) {
1751
2089
  jsonResponse(res, 403, {
1752
- error: "Cannot delete the production theme. Adopt a different theme first.",
1753
- 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"
1754
2092
  });
1755
2093
  return;
1756
2094
  }
1757
- if (import_fs3.default.existsSync(filePath)) {
1758
- import_fs3.default.unlinkSync(filePath);
1759
- if (themesResource.getActiveName() === fileName) {
1760
- themesResource.setActiveName("default");
1761
- }
1762
- }
1763
2095
  jsonResponse(res, 200, { ok: true });
1764
2096
  return;
1765
2097
  }
1766
2098
  }
1767
2099
  async function handleListComponents({ res }) {
1768
- const components = listComponentNames().map((comp) => {
1769
- const r = componentResource(comp);
1770
- return {
1771
- name: comp,
1772
- activeFile: r.getActiveName(),
1773
- productionFile: r.getProductionName()
1774
- };
1775
- });
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
+ }));
1776
2108
  jsonResponse(res, 200, { components });
1777
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
+ }
1778
2115
  async function handleGetComponentActive({ params, res }) {
1779
2116
  const [comp] = params;
1780
- const r = componentResource(comp);
1781
- const activeFile = r.getActiveName();
1782
- const cfg = readComponentConfig(comp, activeFile);
1783
- if (!cfg) {
2117
+ const resolved = resolveLiveComponentConfig(comp, activeTheme());
2118
+ if (!resolved) {
1784
2119
  jsonResponse(res, 404, { error: "Active config not found" });
1785
2120
  return;
1786
2121
  }
1787
- jsonResponse(res, 200, { ...cfg, _fileName: activeFile });
2122
+ jsonResponse(res, 200, {
2123
+ ...resolved.data,
2124
+ _fileName: themesResource.getActiveName(),
2125
+ _source: resolved.source
2126
+ });
1788
2127
  }
1789
- async function handleSetComponentActive({ params, req, res }) {
2128
+ async function handleGetComponentWorking({ params, res }) {
1790
2129
  const [comp] = params;
1791
- const body = JSON.parse(await readBody(req));
1792
- const fileName = sanitizeFileName(body.name || "default");
1793
- const r = componentResource(comp);
1794
- if (r.existingPath(fileName) === null) {
1795
- 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" });
1796
2133
  return;
1797
2134
  }
1798
- r.ensureDir();
1799
- r.setActiveName(fileName);
1800
- jsonResponse(res, 200, { ok: true, activeFile: fileName });
2135
+ jsonResponse(res, 200, raw);
1801
2136
  }
1802
- async function handleGetComponentProduction({ params, res }) {
2137
+ async function handleSetComponentWorking({ params, req, res }) {
1803
2138
  const [comp] = params;
1804
- const r = componentResource(comp);
1805
- const prodFile = r.getProductionName();
1806
- const cfg = readComponentConfig(comp, prodFile);
1807
- jsonResponse(res, 200, {
1808
- fileName: prodFile,
1809
- name: cfg?.name || prodFile,
1810
- aliases: cfg?.aliases || {}
1811
- });
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 });
1812
2144
  }
1813
- async function handleSetComponentProduction({ params, req, res }) {
2145
+ async function handleClearComponentWorking({ params, res }) {
1814
2146
  const [comp] = params;
1815
- const body = JSON.parse(await readBody(req));
1816
- const fileName = sanitizeFileName(body.name || "default");
1817
- const r = componentResource(comp);
1818
- if (r.existingPath(fileName) === null) {
1819
- jsonResponse(res, 404, { error: "Config not found" });
1820
- return;
1821
- }
1822
- if (manifestsResource.getActiveName() === "default") {
1823
- jsonResponse(res, 409, {
1824
- error: "Active manifest is protected. Save As first.",
1825
- code: "ACTIVE_IS_PROTECTED"
1826
- });
1827
- return;
1828
- }
1829
- r.ensureDir();
1830
- r.setProductionName(fileName);
1831
- syncComponentsToCss();
1832
- patchActiveManifest("component", comp, fileName);
1833
- const cfg = readComponentConfig(comp, fileName);
1834
- jsonResponse(res, 200, {
1835
- ok: true,
1836
- fileName,
1837
- name: cfg?.name || fileName,
1838
- aliases: cfg?.aliases || {}
1839
- });
2147
+ componentResource(comp).clearWorking();
2148
+ jsonResponse(res, 200, { ok: true });
1840
2149
  }
1841
2150
  async function handleComponentConfigByName({ params, req, res }) {
1842
2151
  const [comp, name] = params;
2152
+ if (rejectReservedName(res, name)) return;
1843
2153
  const r = componentResource(comp);
1844
2154
  const configPath = r.filePath(name);
1845
2155
  if (req.method === "GET") {
@@ -1862,19 +2172,16 @@ ${lines.join("\n")}
1862
2172
  body.component = comp;
1863
2173
  body.name = body.name || name;
1864
2174
  body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1865
- if (import_fs3.default.existsSync(configPath)) {
2175
+ if (import_fs4.default.existsSync(configPath)) {
1866
2176
  try {
1867
- const existing = JSON.parse(import_fs3.default.readFileSync(configPath, "utf-8"));
2177
+ const existing = JSON.parse(import_fs4.default.readFileSync(configPath, "utf-8"));
1868
2178
  if (existing.createdAt) body.createdAt = existing.createdAt;
1869
2179
  } catch {
1870
2180
  }
1871
2181
  }
1872
2182
  if (!body.createdAt) body.createdAt = body.updatedAt;
1873
2183
  r.ensureDir();
1874
- import_fs3.default.writeFileSync(configPath, JSON.stringify(body, null, 2));
1875
- if (r.getProductionName() === name) {
1876
- syncComponentsToCss();
1877
- }
2184
+ import_fs4.default.writeFileSync(configPath, JSON.stringify(body, null, 2));
1878
2185
  jsonResponse(res, 200, { ok: true, fileName: name });
1879
2186
  return;
1880
2187
  }
@@ -1883,16 +2190,7 @@ ${lines.join("\n")}
1883
2190
  jsonResponse(res, 403, { error: "Cannot delete default config" });
1884
2191
  return;
1885
2192
  }
1886
- if (import_fs3.default.existsSync(configPath)) {
1887
- import_fs3.default.unlinkSync(configPath);
1888
- if (r.getActiveName() === name) {
1889
- r.setActiveName("default");
1890
- }
1891
- if (r.getProductionName() === name) {
1892
- r.setProductionName("default");
1893
- syncComponentsToCss();
1894
- }
1895
- }
2193
+ if (import_fs4.default.existsSync(configPath)) import_fs4.default.unlinkSync(configPath);
1896
2194
  jsonResponse(res, 200, { ok: true });
1897
2195
  return;
1898
2196
  }
@@ -1900,199 +2198,190 @@ ${lines.join("\n")}
1900
2198
  async function handleListComponentConfigs({ params, res }) {
1901
2199
  const [comp] = params;
1902
2200
  const r = componentResource(comp);
1903
- if (!import_fs3.default.existsSync(r.dir)) {
2201
+ if (!import_fs4.default.existsSync(r.dir)) {
1904
2202
  jsonResponse(res, 404, { error: "Component not found" });
1905
2203
  return;
1906
2204
  }
1907
- const activeFile = r.getActiveName();
1908
- const productionFile = r.getProductionName();
1909
2205
  const files = r.listNames().map((fileName) => {
1910
2206
  const data = r.readJson(fileName);
1911
2207
  return {
1912
2208
  name: data?.name || fileName,
1913
2209
  fileName,
1914
- updatedAt: data?.updatedAt || "",
1915
- isActive: fileName === activeFile,
1916
- isProduction: fileName === productionFile
2210
+ updatedAt: data?.updatedAt || ""
1917
2211
  };
1918
2212
  });
1919
- jsonResponse(res, 200, { component: comp, files, activeFile, productionFile });
2213
+ jsonResponse(res, 200, { component: comp, files });
1920
2214
  }
1921
- async function handleListManifests({ res }) {
1922
- const activeFile = manifestsResource.getActiveName();
1923
- const files = manifestsResource.listNames().map((fileName) => {
1924
- const data = manifestsResource.readJson(fileName);
1925
- return {
1926
- 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,
1927
2229
  fileName,
1928
- updatedAt: data?.updatedAt || "",
2230
+ updatedAt: read.theme.updatedAt,
1929
2231
  isActive: fileName === activeFile,
2232
+ isProduction: fileName === productionFile,
1930
2233
  isProtected: fileName === "default"
1931
- };
1932
- });
1933
- jsonResponse(res, 200, { files, activeFile });
2234
+ });
2235
+ }
2236
+ jsonResponse(res, 200, { files, activeFile, productionFile });
2237
+ }
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");
2243
+ return;
2244
+ }
2245
+ jsonResponse(res, 200, { ...read.theme, _fileName: fileName });
1934
2246
  }
1935
- async function handleGetActiveManifest({ res }) {
1936
- const activeFile = manifestsResource.getActiveName();
1937
- const raw = manifestsResource.readJson(activeFile);
1938
- if (!raw) {
1939
- jsonResponse(res, 404, { error: "Active manifest not found" });
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");
1940
2252
  return;
1941
2253
  }
1942
- const data = raw;
1943
- data._fileName = activeFile;
1944
- jsonResponse(res, 200, data);
2254
+ jsonResponse(res, 200, { ...read.theme, _fileName: activeFile });
1945
2255
  }
1946
- async function handleSetActiveManifest({ req, res }) {
2256
+ async function handleSetActiveTheme({ req, res }) {
1947
2257
  const body = JSON.parse(await readBody(req));
1948
2258
  const fileName = sanitizeFileName(body.name || "default");
1949
- if (manifestsResource.existingPath(fileName) === null) {
1950
- jsonResponse(res, 404, { error: "Manifest not found" });
2259
+ if (themesResource.existingPath(fileName) === null) {
2260
+ jsonResponse(res, 404, { error: "Theme not found" });
1951
2261
  return;
1952
2262
  }
1953
- manifestsResource.setActiveName(fileName);
2263
+ themesResource.setActiveName(fileName);
1954
2264
  jsonResponse(res, 200, { ok: true, activeFile: fileName });
1955
2265
  }
1956
- async function handleManifestByName({ params, req, res }) {
2266
+ async function handleThemeByName({ params, req, res }) {
1957
2267
  const [fileName] = params;
1958
- const filePath = manifestsResource.filePath(fileName);
2268
+ if (rejectReservedName(res, fileName)) return;
2269
+ const filePath = themesResource.filePath(fileName);
1959
2270
  if (req.method === "GET") {
1960
- const raw = manifestsResource.readJson(fileName);
1961
- if (!raw) {
1962
- jsonResponse(res, 404, { error: "Not found" });
2271
+ const read = readTheme(fileName);
2272
+ if (!read) {
2273
+ respondUnreadableTheme(res, fileName, "Not found");
1963
2274
  return;
1964
2275
  }
1965
- const data = raw;
1966
- data._fileName = fileName;
1967
- jsonResponse(res, 200, data);
2276
+ jsonResponse(res, 200, { ...read.theme, _fileName: fileName });
1968
2277
  return;
1969
2278
  }
1970
2279
  if (req.method === "PUT") {
1971
2280
  if (fileName === "default") {
1972
- jsonResponse(res, 403, { error: "Cannot overwrite the default manifest" });
2281
+ jsonResponse(res, 403, { error: "Cannot overwrite the default theme" });
1973
2282
  return;
1974
2283
  }
1975
2284
  const body = JSON.parse(await readBody(req));
1976
- body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1977
- 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)) {
1978
2288
  try {
1979
- const existing = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
1980
- 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;
1981
2291
  } catch {
1982
2292
  }
1983
2293
  }
1984
- if (!body.createdAt) body.createdAt = body.updatedAt;
1985
- import_fs3.default.writeFileSync(filePath, JSON.stringify(body, null, 2));
2294
+ writeTheme(fileName, theme);
1986
2295
  jsonResponse(res, 200, { ok: true, fileName });
1987
2296
  return;
1988
2297
  }
1989
2298
  if (req.method === "DELETE") {
1990
2299
  if (fileName === "default") {
1991
- 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
+ });
1992
2308
  return;
1993
2309
  }
1994
- if (import_fs3.default.existsSync(filePath)) {
1995
- import_fs3.default.unlinkSync(filePath);
1996
- if (manifestsResource.getActiveName() === fileName) {
1997
- 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");
1998
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;
1999
2321
  }
2000
2322
  jsonResponse(res, 200, { ok: true });
2001
2323
  return;
2002
2324
  }
2003
2325
  }
2004
- async function handleApplyManifest({ params, res }) {
2326
+ async function handleApplyTheme({ params, res }) {
2005
2327
  const [fileName] = params;
2006
- const manifest = manifestsResource.readJson(fileName);
2007
- if (!manifest) {
2008
- 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");
2009
2332
  return;
2010
2333
  }
2011
- const themeName = sanitizeFileName(manifest.theme || "default");
2012
- if (themesResource.existingPath(themeName) === null) {
2013
- 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" });
2014
2338
  return;
2015
2339
  }
2016
- const knownComponents = new Set(listComponentNames());
2017
- const componentConfigs = manifest.componentConfigs ?? {};
2018
- const resolvedConfigs = {};
2019
- const apply = [];
2020
- for (const [comp, configFile] of Object.entries(componentConfigs)) {
2021
- if (!knownComponents.has(comp)) continue;
2022
- const sanitized = sanitizeFileName(String(configFile) || "default");
2023
- const r = componentResource(comp);
2024
- if (r.existingPath(sanitized) === null) {
2025
- jsonResponse(res, 422, {
2026
- error: `Manifest references missing config: ${comp}/${sanitized}`
2027
- });
2028
- return;
2029
- }
2030
- apply.push([comp, sanitized]);
2031
- }
2032
- themesResource.setActiveName(themeName);
2033
- themesResource.setProductionName(themeName);
2034
- syncTokensToCss(themeName);
2035
- syncFontsToCss(themeName);
2036
- const themeData = normalizeTheme(themesResource.readJson(themeName));
2037
- themeData._fileName = themeName;
2038
- 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];
2039
2345
  const r = componentResource(comp);
2040
- r.setActiveName(configFile);
2041
- r.setProductionName(configFile);
2042
- const cfg = readComponentConfig(comp, configFile);
2043
- if (cfg) resolvedConfigs[comp] = { ...cfg, _fileName: configFile };
2346
+ if (embedded) r.writeWorking({ ...embedded, component: comp });
2347
+ else r.clearWorking();
2044
2348
  }
2349
+ themesResource.setActiveName(fileName);
2350
+ const resolvedConfigs = {};
2045
2351
  for (const comp of knownComponents) {
2046
- if (resolvedConfigs[comp]) continue;
2047
- const activeName = componentResource(comp).getActiveName();
2048
- const cfg = readComponentConfig(comp, activeName);
2049
- 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 };
2050
2354
  }
2051
- syncComponentsToCss();
2052
- manifestsResource.setActiveName(fileName);
2355
+ const resolvedColorsAndType = resolveLiveColorsAndType(theme);
2356
+ const skippedComponents = Object.keys(theme.componentConfigs).filter(
2357
+ (comp) => !knownComponents.includes(comp)
2358
+ );
2053
2359
  jsonResponse(res, 200, {
2054
2360
  ok: true,
2055
- manifest: { ...manifest, _fileName: fileName },
2056
- theme: themeData,
2057
- 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
2058
2369
  });
2059
2370
  }
2060
- async function handleExportManifest({ params, res }) {
2371
+ async function handleExportTheme({ params, res }) {
2061
2372
  const [fileName] = params;
2062
- const manifest = manifestsResource.readJson(fileName);
2063
- if (!manifest) {
2064
- jsonResponse(res, 404, { error: "Manifest not found" });
2065
- return;
2066
- }
2067
- const themeName = sanitizeFileName(manifest.theme || "default");
2068
- const theme = themesResource.readJson(themeName);
2069
- if (!theme) {
2070
- jsonResponse(res, 422, { error: `Manifest references missing theme: ${themeName}` });
2373
+ if (rejectReservedName(res, fileName)) return;
2374
+ const read = readTheme(fileName);
2375
+ if (!read) {
2376
+ respondUnreadableTheme(res, fileName, "Theme not found");
2071
2377
  return;
2072
2378
  }
2073
- const knownComponents = new Set(listComponentNames());
2074
- const componentConfigs = {};
2075
- for (const [comp, configFile] of Object.entries(manifest.componentConfigs ?? {})) {
2076
- if (!knownComponents.has(comp)) continue;
2077
- const sanitized = sanitizeFileName(String(configFile) || "default");
2078
- if (sanitized === "default") continue;
2079
- const cfg = componentResource(comp).readJson(sanitized);
2080
- if (!cfg) {
2081
- jsonResponse(res, 422, {
2082
- error: `Manifest references missing config: ${comp}/${sanitized}`
2083
- });
2084
- return;
2085
- }
2086
- componentConfigs[`${comp}/${sanitized}`] = cfg;
2087
- }
2088
2379
  const bundle = {
2089
- kind: "manifest-bundle",
2090
- schemaVersion: 1,
2380
+ kind: "theme-bundle",
2381
+ schemaVersion: THEME_SCHEMA_VERSION,
2091
2382
  liveTokensVersion: PKG_VERSION,
2092
2383
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2093
- manifest,
2094
- theme,
2095
- componentConfigs
2384
+ manifest: read.theme
2096
2385
  };
2097
2386
  res.statusCode = 200;
2098
2387
  res.setHeader("Content-Type", "application/json");
@@ -2105,7 +2394,7 @@ ${lines.join("\n")}
2105
2394
  function nextAvailableName2(exists, baseName) {
2106
2395
  return nextAvailableName(exists, sanitizeFileName(baseName));
2107
2396
  }
2108
- async function handleImportManifest({ req, res }) {
2397
+ async function handleImportTheme({ req, res }) {
2109
2398
  let bundle;
2110
2399
  try {
2111
2400
  bundle = JSON.parse(await readBody(req));
@@ -2113,117 +2402,112 @@ ${lines.join("\n")}
2113
2402
  jsonResponse(res, 400, { error: "Body is not valid JSON" });
2114
2403
  return;
2115
2404
  }
2116
- if (!bundle || bundle.kind !== "manifest-bundle") {
2405
+ if (!bundle || !Object.values(BUNDLE_KIND_BY_VERSION).includes(bundle.kind)) {
2117
2406
  jsonResponse(res, 400, {
2118
- error: "Not a manifest bundle (kind discriminator missing or wrong)"
2407
+ error: "Not a theme bundle (kind discriminator missing or wrong)"
2119
2408
  });
2120
2409
  return;
2121
2410
  }
2122
- if (bundle.schemaVersion !== 1) {
2411
+ if (bundle.kind !== BUNDLE_KIND_BY_VERSION[bundle.schemaVersion]) {
2123
2412
  jsonResponse(res, 400, {
2124
- error: `Unsupported bundle schemaVersion: ${bundle.schemaVersion}`
2413
+ error: `Unsupported bundle: kind "${bundle.kind}" with schemaVersion ${bundle.schemaVersion}`
2125
2414
  });
2126
2415
  return;
2127
2416
  }
2128
- if (!bundle.manifest || !bundle.theme || !bundle.componentConfigs) {
2129
- jsonResponse(res, 400, { error: "Bundle missing manifest / theme / componentConfigs" });
2417
+ if (!bundle.manifest) {
2418
+ jsonResponse(res, 400, { error: "Bundle missing theme" });
2419
+ return;
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" });
2130
2430
  return;
2131
2431
  }
2432
+ theme.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2132
2433
  const renames = {};
2133
- const now = (/* @__PURE__ */ new Date()).toISOString();
2134
- const originalThemeName = sanitizeFileName(bundle.manifest.theme || "default");
2135
- const finalThemeName = nextAvailableName2(
2434
+ const originalName = sanitizeFileName(theme.name || "imported");
2435
+ const finalName = nextAvailableName2(
2136
2436
  (n) => themesResource.existingPath(n) !== null,
2137
- originalThemeName
2437
+ originalName
2138
2438
  );
2139
- if (finalThemeName !== originalThemeName) {
2140
- renames[`theme:${originalThemeName}`] = finalThemeName;
2439
+ if (finalName !== originalName) {
2440
+ renames[`theme:${originalName}`] = finalName;
2141
2441
  }
2142
- const themeBody = { ...bundle.theme };
2143
- themeBody.updatedAt = now;
2144
- if (!themeBody.createdAt) themeBody.createdAt = now;
2145
- themesResource.ensureDir();
2146
- import_fs3.default.writeFileSync(themesResource.filePath(finalThemeName), JSON.stringify(themeBody, null, 2));
2147
- const knownComponents = new Set(listComponentNames());
2148
- const componentRenames = {};
2149
- for (const [key, cfgValue] of Object.entries(bundle.componentConfigs)) {
2150
- const [comp, originalName] = key.split("/");
2151
- if (!comp || !originalName) continue;
2152
- if (!knownComponents.has(comp)) continue;
2153
- const r = componentResource(comp);
2154
- const finalName = nextAvailableName2(
2155
- (n) => r.existingPath(n) !== null,
2156
- originalName
2157
- );
2158
- if (finalName !== originalName) {
2159
- renames[`componentConfig:${comp}/${originalName}`] = finalName;
2160
- }
2161
- componentRenames[`${comp}/${originalName}`] = finalName;
2162
- const cfgBody = { ...cfgValue };
2163
- cfgBody.component = comp;
2164
- cfgBody.name = finalName;
2165
- cfgBody.updatedAt = now;
2166
- if (!cfgBody.createdAt) cfgBody.createdAt = now;
2167
- r.ensureDir();
2168
- 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;
2169
2458
  }
2170
- const rewrittenManifest = {
2171
- ...bundle.manifest,
2172
- theme: finalThemeName,
2173
- componentConfigs: {}
2174
- };
2175
- for (const [comp, configName] of Object.entries(bundle.manifest.componentConfigs ?? {})) {
2176
- const original = String(configName);
2177
- if (original === "default") {
2178
- rewrittenManifest.componentConfigs[comp] = "default";
2179
- continue;
2180
- }
2181
- const finalName = componentRenames[`${comp}/${original}`] ?? original;
2182
- rewrittenManifest.componentConfigs[comp] = finalName;
2183
- }
2184
- rewrittenManifest.updatedAt = now;
2185
- if (!rewrittenManifest.createdAt) rewrittenManifest.createdAt = now;
2186
- const originalManifestName = sanitizeFileName(bundle.manifest.name || "imported");
2187
- const finalManifestName = nextAvailableName2(
2188
- (n) => manifestsResource.existingPath(n) !== null,
2189
- originalManifestName
2190
- );
2191
- if (finalManifestName !== originalManifestName) {
2192
- renames[`manifest:${originalManifestName}`] = finalManifestName;
2459
+ const read = readTheme(activeName);
2460
+ if (!read) {
2461
+ respondUnreadableTheme(res, activeName, "Active theme not found");
2462
+ return;
2193
2463
  }
2194
- manifestsResource.ensureDir();
2195
- import_fs3.default.writeFileSync(
2196
- manifestsResource.filePath(finalManifestName),
2197
- JSON.stringify(rewrittenManifest, null, 2)
2198
- );
2464
+ themesResource.setProductionName(activeName);
2465
+ regenerateTokensCss();
2466
+ syncFontsToCss(read.theme.colorsAndType);
2199
2467
  jsonResponse(res, 200, {
2200
2468
  ok: true,
2201
- manifest: finalManifestName,
2202
- renames
2469
+ productionTheme: {
2470
+ fileName: activeName,
2471
+ name: read.theme.name,
2472
+ updatedAt: read.theme.updatedAt
2473
+ }
2203
2474
  });
2204
2475
  }
2205
2476
  function methodNotAllowed({ res }) {
2206
2477
  jsonResponse(res, 405, { error: "Method not allowed" });
2207
2478
  }
2479
+ let legacyLayout = null;
2208
2480
  const routes = [
2209
- // Themes — list / active / production are exact strings, must run before THEME_BY_NAME_REGEX
2210
- { method: "GET", pattern: THEMES_ROUTE, handler: handleListThemes },
2211
- { method: "GET", pattern: THEMES_ACTIVE_ROUTE, handler: handleGetActiveTheme },
2212
- { method: "PUT", pattern: THEMES_ACTIVE_ROUTE, handler: handleSetActiveTheme },
2213
- { method: "GET", pattern: THEMES_PRODUCTION_ROUTE, handler: handleGetProductionTheme },
2214
- { 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 },
2215
2494
  // Component configs — list of components
2216
2495
  { method: "GET", pattern: COMPONENT_CONFIGS_ROUTE, handler: handleListComponents },
2217
2496
  // Component configs — :comp/active (must precede :comp/:name)
2218
2497
  { method: "GET", pattern: COMP_ACTIVE_REGEX, handler: handleGetComponentActive },
2219
- { method: "PUT", pattern: COMP_ACTIVE_REGEX, handler: handleSetComponentActive },
2498
+ { method: "PUT", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
2220
2499
  { method: "POST", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
2221
2500
  { method: "DELETE", pattern: COMP_ACTIVE_REGEX, handler: methodNotAllowed },
2222
- // Component configs — :comp/production (must precede :comp/:name)
2223
- { method: "GET", pattern: COMP_PRODUCTION_REGEX, handler: handleGetComponentProduction },
2224
- { 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 },
2225
2504
  { method: "POST", pattern: COMP_PRODUCTION_REGEX, handler: methodNotAllowed },
2226
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 },
2227
2511
  // Component configs — :comp/:name (must precede :comp listing)
2228
2512
  { method: "GET", pattern: COMP_BY_NAME_REGEX, handler: handleComponentConfigByName },
2229
2513
  { method: "PUT", pattern: COMP_BY_NAME_REGEX, handler: handleComponentConfigByName },
@@ -2231,33 +2515,44 @@ ${lines.join("\n")}
2231
2515
  { method: "POST", pattern: COMP_BY_NAME_REGEX, handler: methodNotAllowed },
2232
2516
  // Component configs — list configs for :comp (broadest, runs last among comp routes)
2233
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 },
2234
2547
  // Themes — :name CRUD (broadest theme route, runs last)
2235
2548
  { method: "GET", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
2236
2549
  { method: "PUT", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
2237
2550
  { method: "DELETE", pattern: THEME_BY_NAME_REGEX, handler: handleThemeByName },
2238
- // Manifests — list / active are exact strings, must run before regexes
2239
- { method: "GET", pattern: MANIFESTS_ROUTE, handler: handleListManifests },
2240
- { method: "GET", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleGetActiveManifest },
2241
- { method: "PUT", pattern: MANIFESTS_ACTIVE_ROUTE, handler: handleSetActiveManifest },
2242
- // Manifests — exact import route runs before :name regexes
2243
- { method: "POST", pattern: MANIFEST_IMPORT_ROUTE, handler: handleImportManifest },
2244
- { method: "PUT", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
2245
- { method: "GET", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
2246
- { method: "DELETE", pattern: MANIFEST_IMPORT_ROUTE, handler: methodNotAllowed },
2247
- // Manifests — :name/apply (more specific than :name)
2248
- { method: "PUT", pattern: MANIFEST_APPLY_REGEX, handler: handleApplyManifest },
2249
- { method: "POST", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
2250
- { method: "GET", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
2251
- { method: "DELETE", pattern: MANIFEST_APPLY_REGEX, handler: methodNotAllowed },
2252
- // Manifests — :name/export (more specific than :name)
2253
- { method: "GET", pattern: MANIFEST_EXPORT_REGEX, handler: handleExportManifest },
2254
- { method: "PUT", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
2255
- { method: "POST", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
2256
- { method: "DELETE", pattern: MANIFEST_EXPORT_REGEX, handler: methodNotAllowed },
2257
- // Manifests — :name CRUD (broadest manifest route, runs last)
2258
- { method: "GET", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
2259
- { method: "PUT", pattern: MANIFEST_BY_NAME_REGEX, handler: handleManifestByName },
2260
- { 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 }
2261
2556
  ];
2262
2557
  return {
2263
2558
  name: "theme-file-api",
@@ -2271,19 +2566,42 @@ ${lines.join("\n")}
2271
2566
  };
2272
2567
  },
2273
2568
  configureServer(server) {
2274
- ensureThemesDir();
2275
- ensureComponentConfigsDir();
2276
- ensureManifestsDir();
2277
- regenerateTokensCss();
2278
- if (opts.autoMigrate) autoMigrateAdditive((msg) => server.config.logger.info(msg));
2279
- 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
+ }
2280
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
+ }
2281
2598
  const handled = await dispatch(req, res, routes);
2282
2599
  if (!handled) next();
2283
2600
  });
2284
2601
  },
2285
2602
  handleHotUpdate(ctx) {
2286
- const normalized = import_path3.default.resolve(ctx.file);
2603
+ if (legacyLayout) return;
2604
+ const normalized = import_path4.default.resolve(ctx.file);
2287
2605
  if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
2288
2606
  if (!normalized.endsWith(".svelte")) return;
2289
2607
  if (!isThemeAwareComponent(normalized)) return;