@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
@@ -225,9 +225,53 @@ export function nearestPaletteStep(lightnessCurve: CurveAnchor[], baseL: number)
225
225
  return best;
226
226
  }
227
227
 
228
- /** Insert an anchor at x, or overwrite one already there (endpoints included).
229
- * Returns the y it displaced so the caller can restore it later. */
230
- export function setCurveAnchor(curve: CurveAnchor[], x: number, y: number): { curve: CurveAnchor[]; displacedY?: number } {
228
+ /**
229
+ * Insert an anchor at x, or overwrite one already there (endpoints included).
230
+ * Returns the y it displaced so the caller can restore it later.
231
+ *
232
+ * `smooth` is for a curve's first-ever placement only (nothing has had a
233
+ * chance to edit it yet): every anchor, endpoints included, gets a tangent
234
+ * computed jointly with its real neighbour, so the placement reads as one
235
+ * smooth curve instead of two segments meeting at a corner where the
236
+ * endpoints still aim along their pre-insertion secant. It also returns the
237
+ * untouched original endpoints as `priorEndpoints` — this reshaping isn't
238
+ * algebraically invertible the way the scale-and-preserve path below is, so
239
+ * the caller must hold onto them to restore the curve exactly later. Every
240
+ * other placement (`smooth` false, the default) preserves whatever shape is
241
+ * already there, edited or not.
242
+ */
243
+ export function setCurveAnchor(
244
+ curve: CurveAnchor[],
245
+ x: number,
246
+ y: number,
247
+ smooth = false,
248
+ ): { curve: CurveAnchor[]; displacedY?: number; priorEndpoints?: [CurveAnchor, CurveAnchor] } {
249
+ if (smooth && curve.length === 2) {
250
+ const [p0, p2] = curve;
251
+ const mid: CurveAnchor = { x, y, inDx: 0, inDy: 0, outDx: 0, outDy: 0 };
252
+ if (Math.abs(p0.x - x) < 0.5) {
253
+ return {
254
+ curve: [tangentAnchor(x, y, null, p2), tangentAnchor(p2.x, p2.y, mid, null)],
255
+ displacedY: p0.y,
256
+ priorEndpoints: [p0, p2],
257
+ };
258
+ }
259
+ if (Math.abs(p2.x - x) < 0.5) {
260
+ return {
261
+ curve: [tangentAnchor(p0.x, p0.y, null, mid), tangentAnchor(x, y, p0, null)],
262
+ displacedY: p2.y,
263
+ priorEndpoints: [p0, p2],
264
+ };
265
+ }
266
+ return {
267
+ curve: [
268
+ tangentAnchor(p0.x, p0.y, null, mid),
269
+ tangentAnchor(x, y, p0, p2),
270
+ tangentAnchor(p2.x, p2.y, mid, null),
271
+ ],
272
+ priorEndpoints: [p0, p2],
273
+ };
274
+ }
231
275
  const existing = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
232
276
  if (existing >= 0) {
233
277
  const displacedY = curve[existing].y;
@@ -272,10 +316,22 @@ function fitBetween(a: CurveAnchor, prev: CurveAnchor | null, next: CurveAnchor
272
316
  return out;
273
317
  }
274
318
 
275
- /** Undo a placement at x: restore the displaced y when one was recorded,
276
- * remove the anchor when the placement created it. Endpoints are never
277
- * removed — a curve needs its boundaries. */
278
- export function liftCurveAnchor(curve: CurveAnchor[], x: number, displacedY?: number): CurveAnchor[] {
319
+ /**
320
+ * Undo a placement at x: restore the displaced y when one was recorded,
321
+ * remove the anchor when the placement created it. Endpoints are never
322
+ * removed — a curve needs its boundaries.
323
+ *
324
+ * `priorEndpoints`, when given, wins outright: it's the untouched pair from
325
+ * before a `smooth` placement, which the scale/displace math below can't
326
+ * reconstruct on its own.
327
+ */
328
+ export function liftCurveAnchor(
329
+ curve: CurveAnchor[],
330
+ x: number,
331
+ displacedY?: number,
332
+ priorEndpoints?: [CurveAnchor, CurveAnchor],
333
+ ): CurveAnchor[] {
334
+ if (priorEndpoints) return priorEndpoints;
279
335
  const idx = curve.findIndex((a) => Math.abs(a.x - x) < 0.5);
280
336
  if (idx < 0) return curve;
281
337
  if (displacedY !== undefined) return curve.map((a, i) => (i === idx ? { ...a, y: displacedY } : a));
@@ -303,20 +359,34 @@ export function liftCurveAnchor(curve: CurveAnchor[], x: number, displacedY?: nu
303
359
  export function syncBaseAnchor(cfg: PaletteConfig): void {
304
360
  if (cfg.anchorToBase === false) return;
305
361
  const prev = cfg.anchorPlacement;
362
+ const fresh = prev === undefined;
306
363
  let lCurve = cfg.lightnessCurve;
307
364
  let sCurve = cfg.saturationCurve;
308
365
  if (prev) {
366
+ // Re-placing an existing anchor (a hue move, typically) must preserve
367
+ // whatever is currently on the curve, edited or not — never restore the
368
+ // pristine `priorEndpoints` here, or every hand-tuned handle would be
369
+ // discarded the next time the base color moves.
309
370
  const prevX = stepIndexToX(prev.step);
310
371
  lCurve = liftCurveAnchor(lCurve, prevX, prev.displacedL);
311
372
  sCurve = liftCurveAnchor(sCurve, prevX, prev.displacedS);
312
373
  }
313
374
  const step = nearestPaletteStep(lCurve, cfg.baseColor.l);
314
375
  const x = stepIndexToX(step);
315
- const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100);
316
- const s = setCurveAnchor(sCurve, x, 100);
376
+ const l = setCurveAnchor(lCurve, x, cfg.baseColor.l * 100, fresh);
377
+ const s = setCurveAnchor(sCurve, x, 100, fresh);
317
378
  cfg.lightnessCurve = l.curve;
318
379
  cfg.saturationCurve = s.curve;
319
- cfg.anchorPlacement = { step, displacedL: l.displacedY, displacedS: s.displacedY };
380
+ // priorEndpoints is captured once, on the curve's first-ever placement,
381
+ // and carried forward untouched thereafter so a later clear can still
382
+ // restore the true original regardless of how many times the anchor moved.
383
+ cfg.anchorPlacement = {
384
+ step,
385
+ displacedL: l.displacedY,
386
+ displacedS: s.displacedY,
387
+ priorLightnessEndpoints: prev?.priorLightnessEndpoints ?? l.priorEndpoints,
388
+ priorSaturationEndpoints: prev?.priorSaturationEndpoints ?? s.priorEndpoints,
389
+ };
320
390
  }
321
391
 
322
392
  /** Toggle-off: lift the placement out of both curves and drop the flag. */
@@ -324,8 +394,8 @@ export function clearBaseAnchor(cfg: PaletteConfig): void {
324
394
  const prev = cfg.anchorPlacement;
325
395
  if (prev) {
326
396
  const x = stepIndexToX(prev.step);
327
- cfg.lightnessCurve = liftCurveAnchor(cfg.lightnessCurve, x, prev.displacedL);
328
- cfg.saturationCurve = liftCurveAnchor(cfg.saturationCurve, x, prev.displacedS);
397
+ cfg.lightnessCurve = liftCurveAnchor(cfg.lightnessCurve, x, prev.displacedL, prev.priorLightnessEndpoints);
398
+ cfg.saturationCurve = liftCurveAnchor(cfg.saturationCurve, x, prev.displacedS, prev.priorSaturationEndpoints);
329
399
  cfg.anchorPlacement = undefined;
330
400
  }
331
401
  cfg.anchorToBase = false;
@@ -495,7 +565,7 @@ const HEX_RE = /^#[0-9a-f]{6}$/i;
495
565
  * authoritative anchor and `baseColor` is snapped to it. The flag is then
496
566
  * cleared. Editor-authored palettes (no `_imported`) are left untouched —
497
567
  * see `temp/manifest-robustness-plan.md` §9 for why "snap on any
498
- * divergence" was wrong: it would have flipped `themes/default.json`'s
568
+ * divergence" was wrong: it would have flipped `colors-and-type/default.json`'s
499
569
  * accent from teal to olive on first read.
500
570
  *
501
571
  * - **Consume** (always): every variable the palette's derivation produces
@@ -0,0 +1,141 @@
1
+ import { get } from 'svelte/store';
2
+ import type { FontSource, Theme, ColorsAndType } from '../themes/themeTypes';
3
+ import { editorState, colorsAndTypeToState, toComponentSlice } from '../store/editorStore';
4
+ import { deriveCssVars } from '../store/editorRenderer';
5
+ import { setCssVar, removeCssVar } from '../cssVarSync';
6
+ import { applyFontSources, resolveFontStackValues } from '../fonts/fontLoader';
7
+ import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
8
+ import { loadTheme } from '../themes/themeService';
9
+
10
+ /**
11
+ * Client-side rendering of a saved file, for browsing looks without committing
12
+ * one. A theme previews as a whole look; a colors-and-type file previews as
13
+ * colors and type over the components the user has right now. Nothing here writes to the server,
14
+ * mutates the editor store, or marks anything dirty: a preview is paint only.
15
+ * Save-As stays honest because `captureLook` reads the server's active files,
16
+ * which a preview never touches.
17
+ *
18
+ * Both painting and reverting run the same derivation the renderer runs on
19
+ * store state (`deriveCssVars`), so a preview cannot drift from what an Apply
20
+ * of the same theme produces, and a revert re-derives from the live store
21
+ * rather than replaying DOM values scraped before the preview.
22
+ *
23
+ * One preview is live at a time, whichever kind it is: painting reverts first,
24
+ * so every look is a diff against the user's real state.
25
+ */
26
+
27
+ /** Everything a look puts on the page: the full var set (colors and type,
28
+ * components and the composed `--font-*` stacks) plus the sources its faces
29
+ * come from. */
30
+ export interface RenderedLook {
31
+ vars: Record<string, string>;
32
+ fontSources: FontSource[];
33
+ }
34
+
35
+ /**
36
+ * The full var set a theme paints. Components the theme carries no config
37
+ * for render `defaults`' config, matching Apply's complete-look semantics;
38
+ * configs for components this install lacks are skipped, as Apply skips them.
39
+ */
40
+ export function themeLook(theme: Theme, defaults: Theme): RenderedLook {
41
+ const colorsAndType = structuredClone(theme.colorsAndType);
42
+ migrateColorsAndTypeFonts(colorsAndType);
43
+ const state = colorsAndTypeToState(colorsAndType);
44
+ state.components = {};
45
+ for (const [comp, config] of Object.entries(defaults.componentConfigs)) {
46
+ state.components[comp] = toComponentSlice(comp, config.aliases, config.config, config.schemaVersion);
47
+ }
48
+ for (const [comp, config] of Object.entries(theme.componentConfigs)) {
49
+ if (!(comp in state.components)) continue;
50
+ state.components[comp] = toComponentSlice(comp, config.aliases, config.config, config.schemaVersion);
51
+ }
52
+ const vars = deriveCssVars(state);
53
+ Object.assign(vars, resolveFontStackValues(colorsAndType.fontStacks ?? [], colorsAndType.fontSources ?? []));
54
+ return { vars, fontSources: colorsAndType.fontSources ?? [] };
55
+ }
56
+
57
+ /**
58
+ * The full var set colors and type paint on their own. They are not a whole look,
59
+ * so the components stay as the user has them: `colorsAndTypeToState` carries the live
60
+ * component slice forward (`loadComponentsFromVars`) and strips component-owned
61
+ * vars out of their own bag, which is exactly the composition a preview
62
+ * wants. No defaults fetch, no component reset.
63
+ */
64
+ export function colorsAndTypeLook(colorsAndType: ColorsAndType): RenderedLook {
65
+ const next = structuredClone(colorsAndType);
66
+ migrateColorsAndTypeFonts(next);
67
+ const vars = deriveCssVars(colorsAndTypeToState(next));
68
+ Object.assign(vars, resolveFontStackValues(next.fontStacks ?? [], next.fontSources ?? []));
69
+ return { vars, fontSources: next.fontSources ?? [] };
70
+ }
71
+
72
+ /** The look the editor store currently describes — the state a revert returns to. */
73
+ export function liveLook(): RenderedLook {
74
+ const state = get(editorState);
75
+ const vars = deriveCssVars(state);
76
+ Object.assign(vars, resolveFontStackValues(state.fonts.stacks, state.fonts.sources));
77
+ return { vars, fontSources: state.fonts.sources };
78
+ }
79
+
80
+ let livePreview: RenderedLook | null = null;
81
+ let defaultsPromise: Promise<Theme> | null = null;
82
+
83
+ function loadDefaults(): Promise<Theme> {
84
+ // A rejected promise must not be memoized, or one failed fetch (a dev-server
85
+ // restart mid-dialog) poisons every later preview until a page reload.
86
+ defaultsPromise ??= loadTheme('default').catch((err) => {
87
+ defaultsPromise = null;
88
+ throw err;
89
+ });
90
+ return defaultsPromise;
91
+ }
92
+
93
+ function paint(next: RenderedLook, from: RenderedLook): void {
94
+ for (const [name, value] of Object.entries(next.vars)) {
95
+ if (from.vars[name] !== value) setCssVar(name, value);
96
+ }
97
+ for (const name of Object.keys(from.vars)) {
98
+ if (!(name in next.vars)) removeCssVar(name);
99
+ }
100
+ applyFontSources(next.fontSources);
101
+ }
102
+
103
+ /**
104
+ * Show `look` on the page. Re-entrant: a live preview is reverted first, so
105
+ * every look is painted as a diff against the user's real state and a var one
106
+ * look sets but the next does not returns to its live value instead of vanishing.
107
+ * The two passes run in one task, so the browser never paints the intermediate.
108
+ */
109
+ function applyPreview(look: RenderedLook): void {
110
+ revertPreview();
111
+ paint(look, liveLook());
112
+ livePreview = look;
113
+ }
114
+
115
+ /** Paint a whole look: the theme's colors and type and every component config
116
+ * it carries. */
117
+ export async function previewTheme(theme: Theme): Promise<void> {
118
+ applyPreview(themeLook(theme, await loadDefaults()));
119
+ }
120
+
121
+ /** Paint colors and type over the components as they stand. */
122
+ export function previewColorsAndType(colorsAndType: ColorsAndType): void {
123
+ applyPreview(colorsAndTypeLook(colorsAndType));
124
+ }
125
+
126
+ /** Restore the live editor state. No-op when no preview is running. */
127
+ export function revertPreview(): void {
128
+ if (!livePreview) return;
129
+ paint(liveLook(), livePreview);
130
+ livePreview = null;
131
+ }
132
+
133
+ export function isPreviewing(): boolean {
134
+ return livePreview !== null;
135
+ }
136
+
137
+ /** Test-only: drop the live preview and the cached defaults theme. */
138
+ export function __resetPreviewForTests(): void {
139
+ livePreview = null;
140
+ defaultsPromise = null;
141
+ }
@@ -1,16 +1,12 @@
1
1
  import { writable } from 'svelte/store';
2
- import type { ProductionInfo } from './themes/themeService';
3
- import type { ManifestMeta } from './themes/themeTypes';
2
+ import type { Theme } from './themes/themeTypes';
4
3
 
5
4
  /**
6
- * Monotonic counter that ticks every time a production pointer flips —
7
- * theme production or a component's production. UI surfaces that need to
8
- * react to a sibling Adopt subscribe to this so they refresh without
9
- * per-pair wiring.
5
+ * Monotonic counter that ticks every time an Adopt publishes a theme. UI
6
+ * surfaces that need to react to a sibling Adopt subscribe to this so they
7
+ * refresh without per-pair wiring.
10
8
  *
11
- * Bumpers: `ThemeFileManager.handleApplyToProduction`,
12
- * `ComponentFileManager.handleUpdateProduction`. Anyone setting
13
- * `_production.json` should bump.
9
+ * Bumpers: `ThemePanel.runAdopt`, `ComponentFileManager.handleAdopt`.
14
10
  */
15
11
  export const productionRevision = writable(0);
16
12
 
@@ -19,19 +15,36 @@ export function bumpProductionRevision(): void {
19
15
  }
20
16
 
21
17
  /**
22
- * Cached production-state stores. The Theme and Manifest file managers live in
23
- * the sidebar footer, swapping in/out of the DOM as the user toggles between
24
- * the tokens and components views. Keeping the last-known production state in
25
- * module-level Svelte stores means a remount renders the correct Adopt-button
26
- * state on the first frame instead of flashing through "not in sync" while a
27
- * fresh fetch resolves.
18
+ * Ticks when a component's live config changes on the server — a save, a
19
+ * preset load, a delete. Saving one is visible through `componentDirty`, but
20
+ * loading a preset with no edit in play changes nothing the Theme panel can
21
+ * observe, and its Components count is derived from exactly that. Bumped by
22
+ * `ComponentFileManager`.
28
23
  */
29
- export const themeProductionInfo = writable<ProductionInfo | null>(null);
24
+ export const componentActiveRevision = writable(0);
25
+
26
+ export function bumpComponentActiveRevision(): void {
27
+ componentActiveRevision.update((n) => n + 1);
28
+ }
29
+
30
+ /**
31
+ * The live look has moved past the last bake. Only Adopt bakes the CSS, so a
32
+ * buffer write or a theme save after one leaves production a version behind,
33
+ * and no pointer file records when the bake happened. Every surface reads the
34
+ * one signal: a component save has to reach the theme panel's Adopt, and a
35
+ * component editor's Adopt has to clear it for the panel.
36
+ *
37
+ * Set by the client writes that move the live look (`writeWorkingColorsAndType`,
38
+ * `writeWorkingComponentConfig`, `saveActiveTheme`, `saveAsTheme`); cleared by
39
+ * `adoptLook`. Module-level, so it survives the remounts a view switch causes.
40
+ */
41
+ export const liveMovedSinceBake = writable(false);
30
42
 
31
43
  /**
32
- * Last-known active manifest meta. Bumped by ManifestFileManager whenever the
33
- * active manifest changes (load, save, save-as) and whenever a theme or
34
- * component Adopt completes (the server patches the active manifest as a
35
- * side-effect, so consumers re-read it on `productionRevision` ticks).
44
+ * Last-read production theme — the document `tokens.generated.css` was baked
45
+ * from. The Theme panel and the component file managers live in surfaces that
46
+ * swap in and out of the DOM, so keeping the last answer in a module-level
47
+ * store means a remount renders the correct Adopt state on the first frame
48
+ * instead of flashing through "not in sync" while a fresh fetch resolves.
36
49
  */
37
- export const activeManifest = writable<ManifestMeta | null>(null);
50
+ export const productionTheme = writable<Theme | null>(null);
@@ -1,30 +1,28 @@
1
1
  /**
2
2
  * Client-side REST helpers for any "versioned file resource" — a class of
3
3
  * editable artifact whose lifecycle mirrors the theme files: list / load /
4
- * save / delete + an active pointer + a production pointer.
4
+ * save / delete, plus a live read door and, for themes, the pointer naming the
5
+ * open one.
5
6
  *
6
- * `themeService.ts` and `componentConfigService.ts` previously reimplemented
7
- * the same fetch shape. They now both consume `versionedFileResource(...)`
8
- * with their resource-specific URL.
7
+ * `colorsAndTypeService.ts`, `componentConfigService.ts` and `themeService.ts`
8
+ * all consume `versionedFileResource(...)` with their own URL root.
9
9
  *
10
10
  * Pure URL construction + fetch — no DOM, no filesystem, safe for any browser
11
11
  * or test harness that has a `fetch` global.
12
12
  */
13
13
 
14
14
  export interface VersionedFileResourceClientOptions {
15
- /** REST endpoint root, e.g. `${API_BASE}/themes` or `${API_BASE}/component-configs/button`. */
15
+ /** REST endpoint root, e.g. `${API_BASE}/colors-and-type` or `${API_BASE}/component-configs/button`. */
16
16
  baseUrl: string;
17
17
  }
18
18
 
19
- export interface VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
20
- list(): Promise<{ files: TMeta[]; activeFile?: string; productionFile?: string }>;
19
+ export interface VersionedFileResourceClient<TItem, TMeta> {
20
+ list(): Promise<{ files: TMeta[] }>;
21
21
  load(fileName: string): Promise<TItem>;
22
22
  save(fileName: string, data: TItem): Promise<void>;
23
23
  remove(fileName: string): Promise<void>;
24
24
  getActive(): Promise<TItem | null>;
25
25
  setActive(fileName: string): Promise<void>;
26
- getProductionInfo(): Promise<TProductionInfo>;
27
- setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }>;
28
26
  }
29
27
 
30
28
  /**
@@ -34,17 +32,15 @@ export interface VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
34
32
  * GET {base}/:name → load
35
33
  * PUT {base}/:name → save
36
34
  * DELETE {base}/:name → delete
37
- * GET {base}/active → load active
38
- * PUT {base}/active {name} → set active
39
- * GET {base}/production → production info
40
- * PUT {base}/production {name} → set production
35
+ * GET {base}/active → the live item
36
+ * PUT {base}/active {name} → open that item (themes only)
41
37
  *
42
- * Generic over the payload types so theme & component-config can share the
43
- * same plumbing while keeping their own response/info shapes.
38
+ * Generic over the payload types so themes, colors and type and component
39
+ * configs share the plumbing while keeping their own response shapes.
44
40
  */
45
- export function versionedFileResource<TItem, TMeta, TProductionInfo>(
41
+ export function versionedFileResource<TItem, TMeta>(
46
42
  opts: VersionedFileResourceClientOptions,
47
- ): VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
43
+ ): VersionedFileResourceClient<TItem, TMeta> {
48
44
  const { baseUrl } = opts;
49
45
 
50
46
  async function readJsonError(res: Response, fallback: string): Promise<string> {
@@ -52,7 +48,7 @@ export function versionedFileResource<TItem, TMeta, TProductionInfo>(
52
48
  return err.error || fallback;
53
49
  }
54
50
 
55
- async function list(): Promise<{ files: TMeta[]; activeFile?: string; productionFile?: string }> {
51
+ async function list(): Promise<{ files: TMeta[] }> {
56
52
  const res = await fetch(baseUrl);
57
53
  if (!res.ok) throw new Error(`Failed to list ${baseUrl}`);
58
54
  return res.json();
@@ -105,32 +101,7 @@ export function versionedFileResource<TItem, TMeta, TProductionInfo>(
105
101
  }
106
102
  }
107
103
 
108
- async function getProductionInfo(): Promise<TProductionInfo> {
109
- const res = await fetch(`${baseUrl}/production`);
110
- if (!res.ok) throw new Error('Failed to get production info');
111
- return res.json();
112
- }
113
-
114
- async function setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }> {
115
- const res = await fetch(`${baseUrl}/production`, {
116
- method: 'PUT',
117
- headers: { 'Content-Type': 'application/json' },
118
- body: JSON.stringify({ name: fileName }),
119
- });
120
- if (!res.ok) {
121
- const body = await res.json().catch(() => ({}));
122
- const err = new Error(body.error || 'Set production failed') as Error & {
123
- status?: number;
124
- code?: string;
125
- };
126
- err.status = res.status;
127
- if (body.code) err.code = body.code;
128
- throw err;
129
- }
130
- return res.json();
131
- }
132
-
133
- return { list, load, save, remove, getActive, setActive, getProductionInfo, setProduction };
104
+ return { list, load, save, remove, getActive, setActive };
134
105
  }
135
106
 
136
107
  /** Sanitize a display name to a safe file name. Pure — no DOM, no fs. */
@@ -142,6 +113,8 @@ export function sanitizeFileName(name: string): string {
142
113
  .replace(/\s+/g, '-')
143
114
  .replace(/[^a-z0-9\-_]/g, '')
144
115
  .replace(/-+/g, '-')
145
- .replace(/^-|-$/g, '') || 'unnamed'
116
+ // A leading `_` marks the fixed slots (`_working`, the pointer files), so
117
+ // no name a user types or a bundle carries may keep one.
118
+ .replace(/^[-_]+|-+$/g, '') || 'unnamed'
146
119
  );
147
120
  }
@@ -1,4 +1,4 @@
1
1
  import { writable } from 'svelte/store';
2
2
 
3
- /** The file name of the currently active token file. */
4
- export const activeFileName = writable<string>('default');
3
+ /** Slug of the theme the editor has open — the document every edit belongs to. */
4
+ export const openThemeSlug = writable<string>('default');
@@ -85,7 +85,6 @@ export function normalizeComponents(state: EditorState): EditorState {
85
85
  if (!slice || typeof slice !== 'object') continue;
86
86
  components[name] = {
87
87
  ...slice,
88
- activeFile: typeof slice.activeFile === 'string' ? slice.activeFile : 'default',
89
88
  aliases: slice.aliases && typeof slice.aliases === 'object' ? slice.aliases : {},
90
89
  config: slice.config && typeof slice.config === 'object' ? slice.config : {},
91
90
  };