@motion-proto/live-tokens 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.claude/skills/live-tokens-adjust-shape-space/SKILL.md +68 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-generate-theme/SKILL.md +152 -0
  4. package/CHANGELOG.md +292 -0
  5. package/README.md +37 -26
  6. package/bin/adjust.mjs +254 -0
  7. package/bin/cli.mjs +94 -7
  8. package/bin/generate-theme.mjs +251 -0
  9. package/bin/migrate.mjs +95 -6
  10. package/dist-plugin/adjust/index.cjs +259 -0
  11. package/dist-plugin/adjust/index.d.cts +45 -0
  12. package/dist-plugin/adjust/index.d.ts +45 -0
  13. package/dist-plugin/adjust/index.js +176 -0
  14. package/dist-plugin/{chunk-H4TRUINI.js → chunk-44RSTAII.js} +0 -48
  15. package/dist-plugin/chunk-6OZFXIQI.js +316 -0
  16. package/dist-plugin/chunk-76TFDTJO.js +556 -0
  17. package/dist-plugin/chunk-D3ZVKOR4.js +52 -0
  18. package/dist-plugin/dataPaths-DBN0RPuT.d.cts +54 -0
  19. package/dist-plugin/dataPaths-DBN0RPuT.d.ts +54 -0
  20. package/dist-plugin/generateColorsAndType/index.cjs +1781 -0
  21. package/dist-plugin/generateColorsAndType/index.d.cts +81 -0
  22. package/dist-plugin/generateColorsAndType/index.d.ts +81 -0
  23. package/dist-plugin/generateColorsAndType/index.js +1210 -0
  24. package/dist-plugin/index.cjs +901 -533
  25. package/dist-plugin/index.d.cts +3 -2
  26. package/dist-plugin/index.d.ts +3 -2
  27. package/dist-plugin/index.js +658 -1082
  28. package/dist-plugin/migrateData/index.cjs +728 -0
  29. package/dist-plugin/migrateData/index.d.cts +60 -0
  30. package/dist-plugin/migrateData/index.d.ts +60 -0
  31. package/dist-plugin/migrateData/index.js +348 -0
  32. package/dist-plugin/themeTypes-DMHZOnUn.d.cts +211 -0
  33. package/dist-plugin/themeTypes-DMHZOnUn.d.ts +211 -0
  34. package/dist-plugin/tokensCssMigrations/index.cjs +2 -1
  35. package/dist-plugin/tokensCssMigrations/index.d.cts +3 -15
  36. package/dist-plugin/tokensCssMigrations/index.d.ts +3 -15
  37. package/dist-plugin/tokensCssMigrations/index.js +4 -2
  38. package/package.json +18 -4
  39. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +183 -158
  40. package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +0 -4
  41. package/src/editor/component-editor/scaffolding/SaveAsDialog.svelte +4 -4
  42. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +2 -59
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +1 -1
  44. package/src/editor/core/components/adjustAliases.ts +180 -0
  45. package/src/editor/core/components/aliasKinds.ts +73 -0
  46. package/src/editor/core/components/componentConfigService.ts +26 -38
  47. package/src/editor/core/flashStatus.ts +1 -1
  48. package/src/editor/core/fonts/fontMigration.ts +13 -13
  49. package/src/editor/core/fonts/fontPairing.ts +35 -0
  50. package/src/editor/core/palettes/paletteDerivation.ts +128 -16
  51. package/src/editor/core/preview/lookPreview.ts +141 -0
  52. package/src/editor/core/productionPulse.ts +34 -21
  53. package/src/editor/core/storage/files/versionedFileResourceClient.ts +18 -45
  54. package/src/editor/core/store/editorConfigStore.ts +2 -2
  55. package/src/editor/core/store/editorPersistence.ts +0 -1
  56. package/src/editor/core/store/editorStore.ts +114 -51
  57. package/src/editor/core/store/editorTypes.ts +0 -1
  58. package/src/editor/core/store/gradientSource.ts +2 -2
  59. package/src/editor/core/themes/colorsAndTypeService.ts +95 -0
  60. package/src/editor/core/themes/generateColorsAndType.ts +433 -0
  61. package/src/editor/core/themes/loadRows.ts +64 -0
  62. package/src/editor/core/themes/lookSummary.ts +75 -0
  63. package/src/editor/core/themes/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +3 -3
  64. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -2
  65. package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +3 -3
  66. package/src/editor/core/themes/migrations/2026-08-13-drop-legacy-shape-space-keys.ts +38 -0
  67. package/src/editor/core/themes/migrations/2026-08-15-line-height-scale-rename.ts +57 -0
  68. package/src/editor/core/themes/migrations/index.ts +19 -11
  69. package/src/editor/core/themes/slices/components.ts +4 -4
  70. package/src/editor/core/themes/slices/fonts.ts +1 -1
  71. package/src/editor/core/themes/slices/gradients.ts +22 -0
  72. package/src/editor/core/themes/slices/palettes.ts +2 -2
  73. package/src/editor/core/themes/themeInit.ts +21 -29
  74. package/src/editor/core/themes/themeService.ts +218 -78
  75. package/src/editor/core/themes/themeTypes.ts +94 -51
  76. package/src/editor/docs/Docs.svelte +1 -0
  77. package/src/editor/docs/chapters.ts +1 -0
  78. package/src/editor/docs/content/01-overview.md +1 -1
  79. package/src/editor/docs/content/editing-tokens.md +6 -6
  80. package/src/editor/docs/content/getting-started.md +9 -7
  81. package/src/editor/docs/content/themes-workflow.md +74 -34
  82. package/src/editor/docs/content/where-themes-live.md +61 -0
  83. package/src/editor/docs/content.generated.ts +5 -4
  84. package/src/editor/index.ts +27 -27
  85. package/src/editor/pages/ComponentEditorPage.svelte +2 -2
  86. package/src/editor/pages/EditorShell.svelte +4 -30
  87. package/src/editor/ui/BezierCurveEditor.svelte +2 -2
  88. package/src/editor/ui/ColorEditPanel.svelte +1 -1
  89. package/src/editor/ui/FileLoadList.svelte +67 -7
  90. package/src/editor/ui/GradientEditor.svelte +3 -3
  91. package/src/editor/ui/PaletteEditor.svelte +26 -12
  92. package/src/editor/ui/ThemePanel.svelte +1067 -0
  93. package/src/editor/ui/UIDialog.svelte +11 -9
  94. package/src/editor/ui/UIPillButton.svelte +7 -2
  95. package/src/editor/ui/colors/ColorWheel.svelte +34 -1
  96. package/src/editor/ui/curveEngine.ts +48 -0
  97. package/src/editor/ui/index.ts +4 -2
  98. package/src/editor/ui/palette/PaletteBase.svelte +75 -51
  99. package/src/editor/ui/palette/PaletteJumpButton.svelte +3 -8
  100. package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
  101. package/src/live-tokens/data/{themes → colors-and-type}/default.json +816 -293
  102. package/src/live-tokens/data/colors-and-type/halloween.json +2529 -0
  103. package/src/live-tokens/data/colors-and-type/midnight-study.json +2536 -0
  104. package/src/live-tokens/data/colors-and-type/ocean.json +2489 -0
  105. package/src/live-tokens/data/colors-and-type/royal-velvet.json +2520 -0
  106. package/src/live-tokens/data/colors-and-type/spring-meadow.json +2476 -0
  107. package/src/live-tokens/data/colors-and-type/sunset.json +2528 -0
  108. package/src/live-tokens/data/themes/autumn.json +3918 -0
  109. package/src/live-tokens/data/themes/halloween.json +3992 -0
  110. package/src/live-tokens/data/themes/midnight-study.json +3932 -0
  111. package/src/live-tokens/data/themes/ocean.json +3917 -0
  112. package/src/live-tokens/data/themes/royal-velvet.json +3983 -0
  113. package/src/live-tokens/data/themes/spring-meadow.json +3904 -0
  114. package/src/live-tokens/data/themes/sunset.json +3931 -0
  115. package/src/live-tokens/data/tokens.generated.css +106 -117
  116. package/src/system/styles/CONVENTIONS.md +1 -1
  117. package/src/system/styles/fonts.css +1 -1
  118. package/template/README.md +7 -5
  119. package/template/_gitignore +0 -6
  120. package/src/editor/core/components/componentPersist.ts +0 -62
  121. package/src/editor/core/manifests/manifestService.ts +0 -172
  122. package/src/editor/ui/ManifestFileManager.svelte +0 -446
  123. package/src/editor/ui/ThemeFileManager.svelte +0 -785
  124. package/src/live-tokens/data/manifests/default.json +0 -35
@@ -13,17 +13,18 @@
13
13
  * - `editorRenderer` (DOM subscriber)
14
14
  * - `editorPersistence` (debounced localStorage)
15
15
  *
16
- * Component-config + theme migrations live in `./migrations` (Wave 4).
17
- * `loadComponentActive` / `seedComponentsFromApi` / `loadFromFile` / `toTheme`
16
+ * Component-config + colors-and-type migrations live in `./migrations` (Wave 4).
17
+ * `loadComponentActive` / `seedComponentsFromApi` / `loadFromFile` / `toColorsAndType`
18
18
  * orchestrate across slices and stay here.
19
19
  */
20
20
 
21
+ import { derived, writable, type Readable } from 'svelte/store';
21
22
  import type { CssVarRef, EditorState, GradientAliasValue } from './editorTypes';
22
- import type { AliasDiskValue, Theme } from '../themes/themeTypes';
23
+ import type { AliasDiskValue, ColorsAndType } from '../themes/themeTypes';
23
24
  import { KNOWN_COMPONENT_CONFIG_KEYS } from '../components/componentConfigKeys';
24
25
  import { cssStringToRef } from './cssVarRef';
25
26
  import {
26
- CURRENT_THEME_SCHEMA_VERSION,
27
+ CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
27
28
  CURRENT_COMPONENT_SCHEMA_VERSION,
28
29
  runMigrations,
29
30
  } from '../themes/migrations';
@@ -68,7 +69,11 @@ import {
68
69
  loadShadowsFromVars,
69
70
  shadowsToVars,
70
71
  } from '../themes/slices/shadows';
71
- import { makeDefaultGradients } from '../themes/slices/gradients';
72
+ import {
73
+ gradientsToVars,
74
+ loadGradientsFromFile,
75
+ makeDefaultGradients,
76
+ } from '../themes/slices/gradients';
72
77
  import {
73
78
  componentBaseline,
74
79
  loadComponentsFromVars,
@@ -199,12 +204,12 @@ export {
199
204
  export {
200
205
  setFontSources,
201
206
  setFontStacks,
202
- seedFontsFromTheme,
207
+ seedFontsFromColorsAndType,
203
208
  } from '../themes/slices/fonts';
204
209
 
205
210
  export {
206
211
  setPaletteConfig,
207
- seedPalettesFromTheme,
212
+ seedPalettesFromColorsAndType,
208
213
  } from '../themes/slices/palettes';
209
214
 
210
215
  // ── Component-config load orchestration ───────────────────────────────────
@@ -439,33 +444,44 @@ function splitAliasesAndConfig(
439
444
  }
440
445
 
441
446
  /**
442
- * Replace a component's slice with a loaded config file's contents. Uses
443
- * `mutate()` so the load is one undoable entry; updates the dirty baseline
444
- * so the post-load state reads clean for this component.
447
+ * Disk-shape config → in-memory slice, migrations included. `schemaVersion` is
448
+ * the stamp on the loaded file (0 for legacy files with no stamp); the runner
449
+ * applies any migrations between that and `CURRENT_COMPONENT_SCHEMA_VERSION`,
450
+ * so in-memory state is always at the current version.
445
451
  *
446
- * `schemaVersion` is the stamp on the loaded file (0 for legacy files
447
- * with no stamp). The runner applies any migrations between that and
448
- * `CURRENT_COMPONENT_SCHEMA_VERSION` before the slice is stored — in-memory
449
- * state is always at the current version.
452
+ * Pure: the store paths below commit the result, the theme preview renders
453
+ * one without committing.
454
+ */
455
+ export function toComponentSlice(
456
+ component: string,
457
+ aliases: Record<string, AliasDiskValue>,
458
+ config?: Record<string, unknown>,
459
+ schemaVersion: number = 0,
460
+ ): { aliases: Record<string, CssVarRef>; config: Record<string, unknown> } {
461
+ const migrated = migrateComponentAliases(component, aliases, config, schemaVersion);
462
+ return splitAliasesAndConfig(migrated.aliases, migrated.config);
463
+ }
464
+
465
+ /**
466
+ * Replace a component's slice with a loaded config's contents. Uses `mutate()`
467
+ * so the load is one undoable entry; updates the dirty baseline so the
468
+ * post-load state reads clean for this component.
450
469
  */
451
470
  export function loadComponentActive(
452
471
  component: string,
453
- activeFile: string,
454
472
  aliases: Record<string, AliasDiskValue>,
455
473
  config?: Record<string, unknown>,
456
474
  schemaVersion: number = 0,
457
475
  ): void {
458
- const migrated = migrateComponentAliases(component, aliases, config, schemaVersion);
459
- const split = splitAliasesAndConfig(migrated.aliases, migrated.config);
460
- mutate(`load ${component}/${activeFile}`, (s) => {
461
- s.components[component] = { activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
476
+ const split = toComponentSlice(component, aliases, config, schemaVersion);
477
+ mutate(`load ${component}`, (s) => {
478
+ s.components[component] = { aliases: { ...split.aliases }, config: { ...split.config } };
462
479
  });
463
480
  setSavedComponentBaseline(component, componentBaseline(split));
464
481
  notifyComponentSavedChanged();
465
482
  }
466
483
 
467
484
  export interface ComponentSeed {
468
- activeFile: string;
469
485
  aliases: Record<string, AliasDiskValue>;
470
486
  config?: Record<string, unknown>;
471
487
  schemaVersion?: number;
@@ -484,9 +500,8 @@ export function seedComponentsFromApi(
484
500
  store.update((s) => {
485
501
  s.components = {};
486
502
  for (const [comp, cfg] of Object.entries(configs)) {
487
- const migrated = migrateComponentAliases(comp, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
488
- const split = splitAliasesAndConfig(migrated.aliases, migrated.config);
489
- s.components[comp] = { activeFile: cfg.activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
503
+ const split = toComponentSlice(comp, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
504
+ s.components[comp] = { aliases: { ...split.aliases }, config: { ...split.config } };
490
505
  setSavedComponentBaseline(comp, componentBaseline(split));
491
506
  }
492
507
  return s;
@@ -495,10 +510,10 @@ export function seedComponentsFromApi(
495
510
  schedulePersist();
496
511
  }
497
512
 
498
- // ── Theme load / save ──────────────────────────────────────────────────────
513
+ // ── Colors and type load / save ────────────────────────────────────────────
499
514
 
500
515
  /**
501
- * Per-domain loader: routes a freshly-loaded theme's `cssVariables` bag
516
+ * Per-domain loader: routes a freshly-loaded file's `cssVariables` bag
502
517
  * into typed state on `next`, then removes the routed entries so the
503
518
  * remainder lands as the catch-all `cssVars` bag. Each loader owns its
504
519
  * domain's parser + name list; this table is the only place editorStore
@@ -514,13 +529,14 @@ const domainLoaders: Record<string, DomainLoader> = {
514
529
  };
515
530
 
516
531
  /**
517
- * Replace state with a loaded theme. Clears history and marks saved —
518
- * "open a different document" semantics. Undo cannot cross a theme load.
532
+ * Project a colors-and-type file onto a fresh `EditorState` without committing
533
+ * it. `loadFromFile` commits the projection; the theme preview renders one
534
+ * without touching the store, so both paths derive vars from the same shape.
519
535
  *
520
- * Reads `theme.schemaVersion` (absent = 0) and runs theme migrations up to
521
- * `CURRENT_THEME_SCHEMA_VERSION` before splitting the bag into domains.
536
+ * Reads `schemaVersion` (absent = 0) and runs colors-and-type migrations up to
537
+ * `CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION` before splitting the bag into domains.
522
538
  */
523
- export function loadFromFile(theme: Theme): void {
539
+ export function colorsAndTypeToState(colorsAndType: ColorsAndType): EditorState {
524
540
  const next = emptyState();
525
541
  // Structural palette migrations, innermost → outermost: rename Primary→Brand
526
542
  // and Background→Canvas (both key renames, so they precede anything that looks
@@ -529,38 +545,44 @@ export function loadFromFile(theme: Theme): void {
529
545
  // placement, fold a legacy background spot into the base color, then place any
530
546
  // still-unplaced anchor by base luminance. The clone is still pre-basis (hex
531
547
  // or already-numeric) on disk.
532
- const raw = structuredClone(theme.editorConfigs ?? {}) as Record<string, PreOklchPaletteConfig>;
548
+ const raw = structuredClone(colorsAndType.editorConfigs ?? {}) as Record<string, PreOklchPaletteConfig>;
533
549
  next.palettes = placeUnplacedBaseAnchors(adoptBackgroundSpotAsBase(adoptLegacyBaseAnchor(migratePaletteColorsToOklch(unifyGrayPalettes(renameBackgroundPaletteKey(renamePrimaryPaletteKey(raw)))))));
534
- next.fonts.sources = structuredClone(theme.fontSources ?? []);
535
- next.fonts.stacks = structuredClone(theme.fontStacks ?? []);
550
+ next.fonts.sources = structuredClone(colorsAndType.fontSources ?? []);
551
+ next.fonts.stacks = structuredClone(colorsAndType.fontStacks ?? []);
536
552
  const rawVars = runMigrations(
537
- 'theme',
538
- theme.schemaVersion ?? 0,
539
- theme.cssVariables ?? {},
553
+ 'colors-and-type',
554
+ colorsAndType.schemaVersion ?? 0,
555
+ colorsAndType.cssVariables ?? {},
540
556
  );
541
557
  // Route domain-owned entries out of the catch-all bag into typed state.
542
558
  // Order doesn't matter — each loader claims a disjoint set of var names
543
- // (or in components' case, vars that wouldn't have been in the theme to
559
+ // (or in components' case, vars that wouldn't have been in the file to
544
560
  // begin with).
545
561
  for (const load of Object.values(domainLoaders)) load(next, rawVars);
546
- next.harmonyAxes = sanitizeHarmonyAxes(renameBackgroundHarmonyFamily(theme.harmonyAxes), next.palettes);
562
+ loadGradientsFromFile(next, colorsAndType.gradients, rawVars);
563
+ next.harmonyAxes = sanitizeHarmonyAxes(renameBackgroundHarmonyFamily(colorsAndType.harmonyAxes), next.palettes);
547
564
  next.cssVars = rawVars;
565
+ return next;
566
+ }
567
+
568
+ /**
569
+ * Replace state with a loaded colors-and-type file. Clears history and marks
570
+ * saved — "open a different document" semantics. Undo cannot cross a load.
571
+ */
572
+ export function loadFromFile(colorsAndType: ColorsAndType): void {
573
+ const next = colorsAndTypeToState(colorsAndType);
548
574
  resetHistoryForLoad();
549
575
  store.set(next);
576
+ markColorsAndTypeSaved(next);
550
577
  schedulePersist();
551
578
  }
552
579
 
553
580
  /**
554
- * Serialize current state for saving. Domains with their own typed state
555
- * (columns, overlays, shadows) fold derived vars into `cssVariables` only
556
- * when they diverge from defaults; the catch-all `cssVars` bag carries
557
- * everything not yet migrated to a typed domain.
558
- *
559
- * Stamps the file with `CURRENT_THEME_SCHEMA_VERSION` so future loads can
560
- * skip migrations the file is already past.
581
+ * Everything a colors-and-type file carries except its identity and timestamps.
582
+ * Split out of `toColorsAndType` so the dirty baseline below compares exactly
583
+ * what a save would write, with nothing that changes on its own between two saves.
561
584
  */
562
- export function toTheme(state: EditorState, meta: { name: string }): Theme {
563
- const now = new Date().toISOString();
585
+ function colorsAndTypeContent(state: EditorState): Omit<ColorsAndType, 'name' | 'createdAt' | 'updatedAt'> {
564
586
  const cssVariables: Record<string, string> = { ...state.cssVars };
565
587
  if (!columnsEqualsDefault(state.columns)) {
566
588
  Object.assign(cssVariables, columnsToVars(state.columns));
@@ -569,19 +591,59 @@ export function toTheme(state: EditorState, meta: { name: string }): Theme {
569
591
  if (state.shadows.tokens.length > 0) {
570
592
  Object.assign(cssVariables, shadowsToVars(state.shadows));
571
593
  }
594
+ // Rendered gradient strings ride along in cssVariables for the production
595
+ // CSS pipeline; the structured `gradients` field is the editable basis.
596
+ if (state.gradients.tokens.length > 0) {
597
+ Object.assign(cssVariables, gradientsToVars(state.gradients));
598
+ }
572
599
  return {
573
- name: meta.name,
574
- createdAt: now,
575
- updatedAt: now,
576
600
  editorConfigs: state.palettes,
577
601
  cssVariables,
578
602
  fontSources: state.fonts.sources,
579
603
  fontStacks: state.fonts.stacks,
604
+ gradients: state.gradients.tokens,
580
605
  harmonyAxes: state.harmonyAxes,
581
- schemaVersion: CURRENT_THEME_SCHEMA_VERSION,
606
+ schemaVersion: CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
582
607
  };
583
608
  }
584
609
 
610
+ /**
611
+ * Serialize current state for saving. Domains with their own typed state
612
+ * (columns, overlays, shadows) fold derived vars into `cssVariables` only
613
+ * when they diverge from defaults; the catch-all `cssVars` bag carries
614
+ * everything not yet migrated to a typed domain.
615
+ *
616
+ * Stamps the file with `CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION` so future loads
617
+ * can skip migrations the file is already past.
618
+ */
619
+ export function toColorsAndType(state: EditorState, meta: { name: string }): ColorsAndType {
620
+ const now = new Date().toISOString();
621
+ return { name: meta.name, createdAt: now, updatedAt: now, ...colorsAndTypeContent(state) };
622
+ }
623
+
624
+ // ── Colors-and-type dirty tracking ─────────────────────────────────────────
625
+ //
626
+ // `dirty` counts history entries, and component edits push history too, so it
627
+ // cannot answer whether the colors and type differ from their file. This is
628
+ // the components slice's baseline mechanism at colors-and-type scope: the
629
+ // serialized content of the last file read or written, compared against live
630
+ // state.
631
+
632
+ let savedColorsAndTypeContent = JSON.stringify(colorsAndTypeContent(emptyState()));
633
+ const colorsAndTypeSavedTick = writable(0);
634
+
635
+ export const colorsAndTypeDirty: Readable<boolean> = derived(
636
+ [store, colorsAndTypeSavedTick],
637
+ ([$state]) => JSON.stringify(colorsAndTypeContent($state)) !== savedColorsAndTypeContent,
638
+ );
639
+
640
+ /** Take `state` as what the colors-and-type file now holds. `loadFromFile`
641
+ * passes what it loaded, `persistColorsAndType` what it wrote. */
642
+ export function markColorsAndTypeSaved(state: EditorState): void {
643
+ savedColorsAndTypeContent = JSON.stringify(colorsAndTypeContent(state));
644
+ colorsAndTypeSavedTick.update((n) => n + 1);
645
+ }
646
+
585
647
  // ── Persistence ────────────────────────────────────────────────────────────
586
648
  //
587
649
  // `schedulePersist` + `hydrate` + the eager-hydrate gate live in
@@ -604,6 +666,7 @@ export function __resetForTests(): void {
604
666
  __resetCoreForTests();
605
667
  __resetRendererCacheForTests();
606
668
  __resetComponentsForTests();
669
+ markColorsAndTypeSaved(emptyState());
607
670
  }
608
671
 
609
672
  // ── Wiring ─────────────────────────────────────────────────────────────────
@@ -110,7 +110,6 @@ export type CssVarRef =
110
110
  | { kind: 'gradient'; value: GradientAliasValue };
111
111
 
112
112
  export interface ComponentSlice {
113
- activeFile: string;
114
113
  aliases: Record<string, CssVarRef>;
115
114
  config: Record<string, unknown>;
116
115
  unlinked?: string[];
@@ -53,7 +53,7 @@ export interface GradientSource {
53
53
  }
54
54
 
55
55
  /** Adapter for `--gradient-N` library tokens in `state.gradients.tokens`. */
56
- export function themeGradientSource(variable: string): GradientSource {
56
+ export function colorsAndTypeGradientSource(variable: string): GradientSource {
57
57
  const current = derived(editorState, ($s) => {
58
58
  const t = $s.gradients.tokens.find((g) => g.variable === variable);
59
59
  if (!t) return undefined;
@@ -137,7 +137,7 @@ export function componentGradientSource(component: string, varName: string): Gra
137
137
  * theme-gradient path. */
138
138
  const update = (label: string, mutator: (g: GradientAliasValue) => void): void => {
139
139
  mutate(label, (s) => {
140
- const slice = s.components[component] ?? (s.components[component] = { activeFile: 'default', aliases: {}, config: {} });
140
+ const slice = s.components[component] ?? (s.components[component] = { aliases: {}, config: {} });
141
141
  const ref = slice.aliases[varName];
142
142
  // Seed the editable base from the existing gradient, or lift the current
143
143
  // flat-colour alias into one so the first edit promotes (rather than
@@ -0,0 +1,95 @@
1
+ import { tick } from 'svelte';
2
+ import type { ColorsAndType, ColorsAndTypeMeta } from './themeTypes';
3
+ import type { EditorState } from '../store/editorTypes';
4
+ import {
5
+ versionedFileResource,
6
+ sanitizeFileName as sanitizeFileNameImpl,
7
+ } from '../storage/files/versionedFileResourceClient';
8
+ import { API_BASE } from '../storage/apiBase';
9
+ import { liveMovedSinceBake } from '../productionPulse';
10
+ import { loadFromFile as loadEditorState, toColorsAndType, markSaved, markColorsAndTypeSaved } from '../store/editorStore';
11
+ import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
12
+ import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
13
+
14
+ // ── API helpers ──────────────────────────────────────────────
15
+ //
16
+ // Named colors-and-type files are presets: the user saves, loads and deletes
17
+ // them, and nothing machine-written lands among them. The live colors and type
18
+ // are the `_working` buffer, read back through `/colors-and-type/active`.
19
+
20
+ const colorsAndTypeResource = versionedFileResource<ColorsAndType, ColorsAndTypeMeta>({
21
+ baseUrl: `${API_BASE}/colors-and-type`,
22
+ });
23
+
24
+ export async function listColorsAndType(): Promise<ColorsAndTypeMeta[]> {
25
+ const data = await colorsAndTypeResource.list();
26
+ return data.files;
27
+ }
28
+
29
+ export const loadColorsAndType = (fileName: string): Promise<ColorsAndType> => colorsAndTypeResource.load(fileName);
30
+ export const saveColorsAndType = (fileName: string, data: ColorsAndType): Promise<void> =>
31
+ colorsAndTypeResource.save(fileName, data);
32
+ export const deleteColorsAndType = (fileName: string): Promise<void> => colorsAndTypeResource.remove(fileName);
33
+
34
+ /** The colors and type the page is running: the buffer, else the open theme's
35
+ * copy, else the shipped default. `_source` says which. */
36
+ export async function getActiveColorsAndType(): Promise<ColorsAndType | null> {
37
+ return colorsAndTypeResource.getActive();
38
+ }
39
+
40
+ /** Write the unsaved buffer. Absence of the buffer means "the open theme's
41
+ * saved colors and type", so this is what puts the screen ahead of them. */
42
+ export async function writeWorkingColorsAndType(data: ColorsAndType): Promise<void> {
43
+ const res = await fetch(`${API_BASE}/colors-and-type/working`, {
44
+ method: 'PUT',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify(data),
47
+ });
48
+ if (!res.ok) {
49
+ const err = await res.json().catch(() => ({ error: 'Write failed' }));
50
+ throw new Error(err.error || 'Write failed');
51
+ }
52
+ liveMovedSinceBake.set(true);
53
+ }
54
+
55
+ /** Sanitize a display name to a safe file name. Re-exported from the shared
56
+ * `files/versionedFileResource` so the dev-server plugin can import the
57
+ * canonical pure helper without depending on this module's CSS imports. */
58
+ export const sanitizeFileName = sanitizeFileNameImpl;
59
+
60
+ // ── Colors and type flush / load orchestration ─────────────
61
+ //
62
+ // `persistColorsAndType` and `hydrateColorsAndType` are the canonical entry
63
+ // points for round-tripping editor state through the server. The caller —
64
+ // `ThemePanel`, which flushes the colors and type on screen before it captures
65
+ // or ships — handles only UI-level concerns (status flashing, error chrome).
66
+
67
+ /** Flush the editor state to the buffer under `displayName`, the name of the
68
+ * theme the buffer belongs to, and clear the dirty flag. A capture reads the
69
+ * buffer back, so this is what makes Save and Adopt mean the look on screen. */
70
+ export async function persistColorsAndType(
71
+ state: EditorState,
72
+ displayName: string,
73
+ ): Promise<void> {
74
+ await tick();
75
+ const colorsAndType = toColorsAndType(state, { name: displayName });
76
+ await writeWorkingColorsAndType(colorsAndType);
77
+ markSaved();
78
+ markColorsAndTypeSaved(state);
79
+ }
80
+
81
+ /** Load colors and type into the editor state and re-apply font side-effects
82
+ * (@font-face rules + `--font-*` CSS vars on :root). */
83
+ export function hydrateColorsAndType(colorsAndType: ColorsAndType): void {
84
+ migrateColorsAndTypeFonts(colorsAndType);
85
+ loadEditorState(colorsAndType);
86
+ // Font data is in state.fonts via loadEditorState; the DOM-side-effect
87
+ // helpers still need to run so @font-face rules and --font-* CSS vars
88
+ // land on :root.
89
+ if (colorsAndType.fontSources && colorsAndType.fontSources.length > 0) {
90
+ applyFontSources(colorsAndType.fontSources);
91
+ }
92
+ if (colorsAndType.fontStacks && colorsAndType.fontStacks.length > 0) {
93
+ applyFontStacks(colorsAndType.fontStacks, colorsAndType.fontSources ?? []);
94
+ }
95
+ }