@motion-proto/live-tokens 0.49.1 → 0.52.1

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 (111) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist-plugin/adjust/index.d.cts +1 -1
  3. package/dist-plugin/adjust/index.d.ts +1 -1
  4. package/dist-plugin/{chunk-76TFDTJO.js → chunk-JIGIIE5P.js} +60 -132
  5. package/dist-plugin/{chunk-44RSTAII.js → chunk-TKZBVIW5.js} +33 -5
  6. package/dist-plugin/chunk-YR6GPXW2.js +129 -0
  7. package/dist-plugin/generateColorsAndType/index.cjs +100 -33
  8. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  9. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  10. package/dist-plugin/generateColorsAndType/index.js +35 -14
  11. package/dist-plugin/index.cjs +95 -55
  12. package/dist-plugin/index.js +5 -3
  13. package/dist-plugin/{themeTypes-DSwKq-bj.d.cts → themeTypes-Dky03HL9.d.cts} +18 -5
  14. package/dist-plugin/{themeTypes-DSwKq-bj.d.ts → themeTypes-Dky03HL9.d.ts} +18 -5
  15. package/dist-plugin/tokensCssMigrations/index.cjs +71 -5
  16. package/dist-plugin/tokensCssMigrations/index.d.cts +20 -9
  17. package/dist-plugin/tokensCssMigrations/index.d.ts +20 -9
  18. package/dist-plugin/tokensCssMigrations/index.js +4 -1
  19. package/package.json +8 -1
  20. package/src/editor/bootstrap.ts +2 -0
  21. package/src/editor/component-editor/ButtonEditor.svelte +1 -1
  22. package/src/editor/component-editor/CardEditor.svelte +5 -2
  23. package/src/editor/component-editor/IconButtonEditor.svelte +1 -1
  24. package/src/editor/component-editor/ImageEditor.svelte +1 -1
  25. package/src/editor/component-editor/NotificationEditor.svelte +6 -0
  26. package/src/editor/component-editor/SectionDividerEditor.svelte +4 -1
  27. package/src/editor/component-editor/SideNavigationEditor.svelte +6 -0
  28. package/src/editor/component-editor/TabBarEditor.svelte +1 -5
  29. package/src/editor/component-editor/scaffolding/ComponentEditorBase.svelte +8 -1
  30. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -32
  31. package/src/editor/component-editor/scaffolding/StateBlock.svelte +2 -0
  32. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +3 -1
  33. package/src/editor/component-editor/scaffolding/types.ts +3 -0
  34. package/src/editor/core/components/componentConfigService.ts +25 -0
  35. package/src/editor/core/cssVarSync.ts +65 -7
  36. package/src/editor/core/fonts/fontLoader.ts +12 -3
  37. package/src/editor/core/fonts/fontWeightAvailability.ts +97 -0
  38. package/src/editor/core/palettes/oklch.ts +3 -137
  39. package/src/editor/core/palettes/paletteDerivation.ts +79 -37
  40. package/src/editor/core/preview/lookPreview.ts +23 -18
  41. package/src/editor/core/store/editorRenderer.ts +22 -7
  42. package/src/editor/core/store/editorStore.ts +31 -0
  43. package/src/editor/core/store/gradientSource.ts +5 -0
  44. package/src/editor/core/themes/colorsAndTypeService.ts +2 -12
  45. package/src/editor/core/themes/generateColorsAndType.ts +18 -8
  46. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +4 -1
  47. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +4 -1
  48. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +8 -0
  49. package/src/editor/core/themes/slices/fonts.ts +4 -5
  50. package/src/editor/core/themes/themeDocumentSync.ts +66 -0
  51. package/src/editor/core/themes/themeInit.ts +7 -10
  52. package/src/editor/core/themes/themeService.ts +10 -3
  53. package/src/editor/core/themes/themeTypes.ts +18 -6
  54. package/src/editor/docs/content/editing-tokens.md +6 -2
  55. package/src/editor/docs/content/themes-workflow.md +14 -11
  56. package/src/editor/docs/content/where-themes-live.md +2 -2
  57. package/src/editor/docs/content.generated.ts +3 -3
  58. package/src/editor/ui/BezierCurveEditor.svelte +90 -67
  59. package/src/editor/ui/FileLoadList.svelte +1 -0
  60. package/src/editor/ui/FontStackEditor.svelte +1 -2
  61. package/src/editor/ui/GradientEditor.svelte +5 -1
  62. package/src/editor/ui/PaletteEditor.svelte +150 -44
  63. package/src/editor/ui/ProjectFontsSection.svelte +0 -5
  64. package/src/editor/ui/ThemePanel.svelte +206 -42
  65. package/src/editor/ui/Toggle.svelte +14 -2
  66. package/src/editor/ui/UIDialog.svelte +7 -1
  67. package/src/editor/ui/UIFontFamilySelector.svelte +13 -13
  68. package/src/editor/ui/UIFontSizeSelector.svelte +5 -5
  69. package/src/editor/ui/UIFontWeightSelector.svelte +52 -2
  70. package/src/editor/ui/UIOptionItem.svelte +14 -1
  71. package/src/editor/ui/UIPaddingSelector.svelte +4 -3
  72. package/src/editor/ui/UIPaletteSelector.svelte +20 -13
  73. package/src/editor/ui/UIRelinkConfirmDialog.svelte +18 -3
  74. package/src/editor/ui/UITextTransformSelector.svelte +5 -5
  75. package/src/editor/ui/UITokenSelector.svelte +29 -7
  76. package/src/editor/ui/colors/ColorStory.svelte +12 -8
  77. package/src/editor/ui/curveEngine.ts +101 -21
  78. package/src/editor/ui/palette/OverridesPanel.svelte +19 -5
  79. package/src/editor/ui/palette/PaletteBase.svelte +17 -78
  80. package/src/editor/ui/palette/ScaleCurveEditor.svelte +109 -20
  81. package/src/editor/ui/palette/curveSummary.ts +20 -0
  82. package/src/editor/ui/palette/dockMagnify.ts +5 -13
  83. package/src/editor/ui/palette/paletteMath.ts +7 -4
  84. package/src/live-tokens/data/colors-and-type/autumn.json +528 -528
  85. package/src/live-tokens/data/colors-and-type/default.json +247 -247
  86. package/src/live-tokens/data/colors-and-type/halloween.json +532 -532
  87. package/src/live-tokens/data/colors-and-type/midnight-study.json +538 -538
  88. package/src/live-tokens/data/colors-and-type/ocean.json +521 -521
  89. package/src/live-tokens/data/colors-and-type/royal-velvet.json +532 -532
  90. package/src/live-tokens/data/colors-and-type/spring-meadow.json +504 -504
  91. package/src/live-tokens/data/colors-and-type/sunset.json +531 -531
  92. package/src/live-tokens/data/themes/autumn.json +527 -527
  93. package/src/live-tokens/data/themes/halloween.json +531 -531
  94. package/src/live-tokens/data/themes/midnight-study.json +537 -537
  95. package/src/live-tokens/data/themes/ocean.json +520 -520
  96. package/src/live-tokens/data/themes/royal-velvet.json +531 -531
  97. package/src/live-tokens/data/themes/spring-meadow.json +516 -514
  98. package/src/live-tokens/data/themes/sunset.json +531 -531
  99. package/src/live-tokens/data/tokens.generated.css +282 -282
  100. package/src/system/components/Button.svelte +2 -1
  101. package/src/system/components/FloatingTokenTags.css +2 -2
  102. package/src/system/components/FloatingTokenTags.svelte +11 -0
  103. package/src/system/components/IconButton.svelte +2 -1
  104. package/src/system/components/Image.svelte +9 -3
  105. package/src/system/components/ImageLightbox.svelte +2 -0
  106. package/src/system/components/SectionDivider.svelte +20 -2
  107. package/src/system/components/SideNavigation.svelte +15 -1
  108. package/src/system/internal/backgroundContrast.ts +7 -0
  109. package/src/system/internal/oklch.ts +189 -0
  110. package/src/system/styles/fonts.css +6 -6
  111. package/src/system/styles/tokens.css +283 -283
@@ -76,6 +76,7 @@ import {
76
76
  } from '../themes/slices/gradients';
77
77
  import {
78
78
  componentBaseline,
79
+ getComponentOwnedVarNames,
79
80
  loadComponentsFromVars,
80
81
  notifyComponentSavedChanged,
81
82
  setSavedComponentBaseline,
@@ -510,6 +511,36 @@ export function seedComponentsFromApi(
510
511
  schedulePersist();
511
512
  }
512
513
 
514
+ /**
515
+ * Replace colors/type and every component slice as one theme transaction.
516
+ * Theme application used to call `loadFromFile` and `seedComponentsFromApi`
517
+ * separately, causing two complete renderer passes and briefly exposing a
518
+ * mixed old-components/new-tokens state. Build the final state first and emit
519
+ * it once instead.
520
+ */
521
+ export function loadThemeFromApi(
522
+ colorsAndType: ColorsAndType,
523
+ configs: Record<string, ComponentSeed>,
524
+ ): void {
525
+ const next = colorsAndTypeToState(colorsAndType);
526
+ next.components = {};
527
+ for (const [comp, cfg] of Object.entries(configs)) {
528
+ const split = toComponentSlice(comp, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
529
+ next.components[comp] = { aliases: { ...split.aliases }, config: { ...split.config } };
530
+ setSavedComponentBaseline(comp, componentBaseline(split));
531
+ }
532
+ // Defensive legacy cleanup: a colors-and-type payload may still carry
533
+ // component-owned variables in its catch-all bag. The component slice is
534
+ // authoritative for a whole-theme load.
535
+ for (const name of getComponentOwnedVarNames(next)) delete next.cssVars[name];
536
+
537
+ resetHistoryForLoad();
538
+ store.set(next);
539
+ markColorsAndTypeSaved(next);
540
+ notifyComponentSavedChanged();
541
+ schedulePersist();
542
+ }
543
+
513
544
  // ── Colors and type load / save ────────────────────────────────────────────
514
545
 
515
546
  /**
@@ -40,6 +40,9 @@ export interface GradientSourceSnapshot {
40
40
  }
41
41
 
42
42
  export interface GradientSource {
43
+ /** CSS variable ultimately written by this source. Exposed for editor
44
+ * semantics and automated control-to-runtime contract discovery. */
45
+ targetVariable: string;
43
46
  /** Reactive readable of the current gradient value (or undefined if missing). */
44
47
  current: Readable<GradientSourceSnapshot | undefined>;
45
48
  setAll(next: GradientSourceSnapshot): void;
@@ -67,6 +70,7 @@ export function colorsAndTypeGradientSource(variable: string): GradientSource {
67
70
  };
68
71
  });
69
72
  return {
73
+ targetVariable: variable,
70
74
  current,
71
75
  setAll: (next) => setGradient(variable, next),
72
76
  setType: (t) => setGradientType(variable, t),
@@ -158,6 +162,7 @@ export function componentGradientSource(component: string, varName: string): Gra
158
162
  });
159
163
  };
160
164
  return {
165
+ targetVariable: varName,
161
166
  current,
162
167
  setAll: (next) => writeComponentGradient(component, varName, {
163
168
  type: next.type,
@@ -8,7 +8,6 @@ import {
8
8
  import { API_BASE } from '../storage/apiBase';
9
9
  import { liveMovedSinceBake } from '../productionPulse';
10
10
  import { loadFromFile as loadEditorState, toColorsAndType, markSaved, markColorsAndTypeSaved } from '../store/editorStore';
11
- import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
12
11
  import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
13
12
 
14
13
  // ── API helpers ──────────────────────────────────────────────
@@ -78,18 +77,9 @@ export async function persistColorsAndType(
78
77
  markColorsAndTypeSaved(state);
79
78
  }
80
79
 
81
- /** Load colors and type into the editor state and re-apply font side-effects
82
- * (@font-face rules + `--font-*` CSS vars on :root). */
80
+ /** Load colors and type into the editor state. The store renderer projects
81
+ * font sources, stacks, and all other CSS variables in one reactive pass. */
83
82
  export function hydrateColorsAndType(colorsAndType: ColorsAndType): void {
84
83
  migrateColorsAndTypeFonts(colorsAndType);
85
84
  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
85
  }
@@ -5,7 +5,7 @@
5
5
  * `dist-plugin/generateColorsAndType`, so keep this module Node-safe (no DOM).
6
6
  */
7
7
 
8
- import { hexToOklch, type Oklch } from '../palettes/oklch';
8
+ import { hexToOklch, cssColorToOklch, oklchToHexClamped, type Oklch } from '../palettes/oklch';
9
9
  import {
10
10
  PALETTE_SPECS,
11
11
  PALETTE_STEPS,
@@ -201,6 +201,15 @@ function surfaceVars(): string[] {
201
201
  ];
202
202
  }
203
203
 
204
+ /** Derived vars serialize as `oklch()` while the WCAG contrast API is
205
+ * sRGB-terminated (decision 7), so project back to hex at this boundary.
206
+ * Gradient `--page-bg` parses to null and is skipped, as it was when these
207
+ * call sites read hex directly. */
208
+ function varToHex(value: string | undefined): string | null {
209
+ const parsed = value ? cssColorToOklch(value) : null;
210
+ return parsed ? oklchToHexClamped(parsed.l, parsed.c, parsed.h) : null;
211
+ }
212
+
204
213
  function worstSurface(
205
214
  textHex: string,
206
215
  vars: Record<string, string>,
@@ -208,8 +217,8 @@ function worstSurface(
208
217
  let against = '';
209
218
  let ratio = Infinity;
210
219
  for (const surfVar of surfaceVars()) {
211
- const surfHex = vars[surfVar];
212
- if (!surfHex || !HEX_RE.test(surfHex)) continue;
220
+ const surfHex = varToHex(vars[surfVar]);
221
+ if (!surfHex) continue;
213
222
  const r = contrastRatio(textHex, surfHex);
214
223
  if (r < ratio) {
215
224
  ratio = r;
@@ -248,17 +257,18 @@ function runContrastGate(
248
257
  const vars = palettesToVars(configs);
249
258
  let anyFixed = false;
250
259
  for (const def of defs) {
251
- const textHex = vars[def.textVar];
252
- if (!textHex) continue;
260
+ const text = cssColorToOklch(vars[def.textVar] ?? '');
261
+ if (!text) continue;
262
+ const textHex = oklchToHexClamped(text.l, text.c, text.h);
253
263
  const worst = worstSurface(textHex, vars);
254
264
  if (worst.ratio >= def.floor) continue;
255
265
  const cfg = configs[def.paletteLabel];
256
266
  const seed = cfg.baseColor;
257
267
  const solved = findLForContrast({
258
- against: vars[worst.against],
268
+ against: varToHex(vars[worst.against]) ?? '#000000',
259
269
  ratio: def.target,
260
270
  direction,
261
- c: hexToOklch(textHex).c,
271
+ c: text.c,
262
272
  h: seed.h,
263
273
  lMax: Math.min(1, 2 * seed.l),
264
274
  });
@@ -271,7 +281,7 @@ function runContrastGate(
271
281
 
272
282
  const finalVars = palettesToVars(configs);
273
283
  const checks: ContrastCheck[] = defs.map((def) => {
274
- const worst = worstSurface(finalVars[def.textVar] ?? '#000000', finalVars);
284
+ const worst = worstSurface(varToHex(finalVars[def.textVar]) ?? '#000000', finalVars);
275
285
  return {
276
286
  textVar: def.textVar,
277
287
  floor: def.floor,
@@ -28,7 +28,10 @@ const VARIANTS = ['lg', 'md', 'sm'] as const;
28
28
  const SUFFIX_RENAMES: Array<[RegExp, string]> = [
29
29
  [/-title-border-width$/, '-title-outline-width'],
30
30
  [/-title-stroke-color$/, '-title-outline-color'],
31
- [/-padding$/, '-spacing'],
31
+ [
32
+ /^--sectiondivider-(canvas|neutral|alternate|primary|accent|special)-padding$/,
33
+ '--sectiondivider-$1-spacing',
34
+ ],
32
35
  ];
33
36
 
34
37
  function renameKeySuffix(key: string): string {
@@ -16,7 +16,10 @@ export const componentMigration_2026_05_21_sectiondividerSpacingToPadding: Migra
16
16
  if (meta.component !== 'sectiondivider') return { ...rawVars };
17
17
  const out: Record<string, string> = {};
18
18
  for (const [key, value] of Object.entries(rawVars)) {
19
- const renamed = key.replace(/-spacing$/, '-padding');
19
+ const renamed = key.replace(
20
+ /^--sectiondivider-(lg|md|sm)-spacing$/,
21
+ '--sectiondivider-$1-padding',
22
+ );
20
23
  out[renamed] = value;
21
24
  }
22
25
  return out;
@@ -16,6 +16,7 @@ import type { Migration } from './index';
16
16
  */
17
17
  const VARIANTS = ['primary', 'accent', 'neutral', 'alternate', 'canvas', 'special', 'success', 'warning', 'danger', 'info'] as const;
18
18
  const RE = new RegExp(`^--corner-badge-(${VARIANTS.join('|')})-(.+)$`);
19
+ const CURRENT_COLOR_PROPS = new Set(['surface', 'border', 'text']);
19
20
 
20
21
  function flattenVariants(rawVars: Record<string, string>, meta: { component?: string }): Record<string, string> {
21
22
  if (meta.component !== 'cornerbadge') return rawVars;
@@ -25,6 +26,13 @@ function flattenVariants(rawVars: Record<string, string>, meta: { component?: st
25
26
  const m = key.match(RE);
26
27
  if (m) {
27
28
  const prop = m[2];
29
+ // The current component composes Badge's per-variant colors under these
30
+ // same prefixes. They were introduced after this migration and must be
31
+ // left intact when an otherwise-current unversioned snapshot is loaded.
32
+ if (CURRENT_COLOR_PROPS.has(prop)) {
33
+ out[key] = value;
34
+ continue;
35
+ }
28
36
  if (!collected.has(prop)) collected.set(prop, value);
29
37
  continue;
30
38
  }
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Fonts slice — sources + stacks. No derived CSS vars owned by this store
3
- * (the `--font-*` vars are written by `applyFontStacks` in fontLoader, and
4
- * @font-face rules by `applyFontSources`). We own the *data* (sources +
5
- * stacks); callers still invoke the DOM-side-effect helpers themselves
6
- * after mutating.
2
+ * Fonts slice — sources + stacks. The editor renderer projects both from the
3
+ * store: stacks become `--font-*` vars and sources become mirrored
4
+ * <link>/<style> nodes. Callers only mutate data; undo/redo and hydration then
5
+ * use the exact same reactive path as the initial edit.
7
6
  */
8
7
  import type { FontSource, FontStack } from '../themeTypes';
9
8
  import { store, mutate, persist } from '../../store/editorCore';
@@ -0,0 +1,66 @@
1
+ import { liveMovedSinceBake } from '../productionPulse';
2
+ import { openThemeSlug } from '../store/editorConfigStore';
3
+ import { loadThemeFromApi } from '../store/editorStore';
4
+ import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
5
+ import type { ApplyThemeResult } from './themeService';
6
+
7
+ const CHANNEL_NAME = 'live-tokens:active-theme:v1';
8
+ export const THEME_APPLIED_EVENT = 'live-tokens:theme-applied';
9
+
10
+ export interface AppliedThemeDetail {
11
+ fileName: string;
12
+ result: ApplyThemeResult;
13
+ }
14
+
15
+ interface AppliedThemeMessage {
16
+ type: 'theme-applied';
17
+ fileName: string;
18
+ result: ApplyThemeResult;
19
+ }
20
+
21
+ let channel: BroadcastChannel | null = null;
22
+
23
+ function isAppliedThemeMessage(value: unknown): value is AppliedThemeMessage {
24
+ if (!value || typeof value !== 'object') return false;
25
+ const message = value as Partial<AppliedThemeMessage>;
26
+ return message.type === 'theme-applied'
27
+ && typeof message.fileName === 'string'
28
+ && !!message.result
29
+ && typeof message.result === 'object'
30
+ && !!message.result.colorsAndType
31
+ && typeof message.result.componentConfigs === 'object';
32
+ }
33
+
34
+ /**
35
+ * Replace this document's editor state with the complete state returned by
36
+ * the theme apply endpoint. Each same-origin document owns its own Svelte
37
+ * store, so writing CSS into another document is not enough: the receiving
38
+ * editor must hydrate the typed palette and component layers as well.
39
+ */
40
+ export function hydrateAppliedTheme(fileName: string, result: ApplyThemeResult): void {
41
+ const colorsAndType = structuredClone(result.colorsAndType);
42
+ migrateColorsAndTypeFonts(colorsAndType);
43
+ loadThemeFromApi(colorsAndType, structuredClone(result.componentConfigs));
44
+ openThemeSlug.set(fileName);
45
+ liveMovedSinceBake.set(false);
46
+ if (typeof document !== 'undefined') {
47
+ document.dispatchEvent(new CustomEvent<AppliedThemeDetail>(THEME_APPLIED_EVENT, {
48
+ detail: { fileName, result },
49
+ }));
50
+ }
51
+ }
52
+
53
+ /** Install the same-origin theme bridge for this window or iframe. */
54
+ export function init(): void {
55
+ if (channel || typeof BroadcastChannel === 'undefined') return;
56
+ channel = new BroadcastChannel(CHANNEL_NAME);
57
+ channel.addEventListener('message', (event: MessageEvent<unknown>) => {
58
+ if (!isAppliedThemeMessage(event.data)) return;
59
+ hydrateAppliedTheme(event.data.fileName, event.data.result);
60
+ });
61
+ }
62
+
63
+ /** Notify every already-open editor/host document after the server switches. */
64
+ export function broadcastAppliedTheme(fileName: string, result: ApplyThemeResult): void {
65
+ channel?.postMessage({ type: 'theme-applied', fileName, result } satisfies AppliedThemeMessage);
66
+ }
@@ -1,7 +1,6 @@
1
1
  import type { AliasDiskValue, ColorsAndType } from './themeTypes';
2
2
  import { openThemeSlug } from '../store/editorConfigStore';
3
3
  import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
4
- import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
5
4
  import { loadFromFile, seedComponentsFromApi } from '../store/editorStore';
6
5
  import { getActiveComponentConfig, type ComponentSummary } from '../components/componentConfigService';
7
6
  import { safeFetch } from '../storage/storage';
@@ -33,12 +32,6 @@ export async function initializeTheme(): Promise<void> {
33
32
  if (colorsAndType) {
34
33
  migrateColorsAndTypeFonts(colorsAndType);
35
34
  loadFromFile(colorsAndType);
36
- if (colorsAndType.fontSources && colorsAndType.fontSources.length > 0) {
37
- applyFontSources(colorsAndType.fontSources);
38
- }
39
- if (colorsAndType.fontStacks && colorsAndType.fontStacks.length > 0) {
40
- applyFontStacks(colorsAndType.fontStacks, colorsAndType.fontSources ?? []);
41
- }
42
35
  openThemeSlug.set(colorsAndType._fileName || 'default');
43
36
  }
44
37
 
@@ -48,6 +41,7 @@ export async function initializeTheme(): Promise<void> {
48
41
  string,
49
42
  { aliases: Record<string, AliasDiskValue>; config?: Record<string, unknown>; schemaVersion?: number }
50
43
  > = {};
44
+ let componentReadFailed = false;
51
45
  await Promise.all(
52
46
  list.components.map(async (c) => {
53
47
  const cfg = await getActiveComponentConfig(c.name);
@@ -57,11 +51,14 @@ export async function initializeTheme(): Promise<void> {
57
51
  config: cfg.config,
58
52
  schemaVersion: cfg.schemaVersion,
59
53
  };
54
+ } else {
55
+ componentReadFailed = true;
60
56
  }
61
57
  }),
62
58
  );
63
- if (Object.keys(configs).length > 0) {
64
- seedComponentsFromApi(configs);
65
- }
59
+ // A successful empty list is authoritative. A partial read is not: avoid
60
+ // replacing the visible design system with a mixture of active configs
61
+ // and CSS defaults because one request happened to fail during boot.
62
+ if (!componentReadFailed) seedComponentsFromApi(configs);
66
63
  }
67
64
  }
@@ -4,6 +4,7 @@ import { API_BASE } from '../storage/apiBase';
4
4
  import { liveMovedSinceBake } from '../productionPulse';
5
5
  import { listComponents, getActiveComponentConfig } from '../components/componentConfigService';
6
6
  import { getActiveColorsAndType } from './colorsAndTypeService';
7
+ import { broadcastAppliedTheme, hydrateAppliedTheme } from './themeDocumentSync';
7
8
 
8
9
  /**
9
10
  * REST client for theme files, the documents of the editor. A theme carries a
@@ -67,8 +68,11 @@ export interface ApplyThemeResult {
67
68
  * `themes/_active.json` at it, and returns the resolved state in one payload.
68
69
  * Live reads then fall through to the theme's embedded layers.
69
70
  * Production is untouched, so trying a look cannot change what the site ships.
70
- * Clients follow with a full page reload; opening a theme is a "blow up the
71
- * world" action.
71
+ * The resolved response hydrates the caller immediately, then broadcasts the
72
+ * same typed state to every already-open same-origin host/editor document.
73
+ * Each store renderer projects that state into its own document, so a theme
74
+ * loaded from either side keeps the controls, aliases, and visible components
75
+ * on the same design system without a reload.
72
76
  */
73
77
  export async function applyTheme(fileName: string): Promise<ApplyThemeResult> {
74
78
  const res = await fetch(`${API_BASE}/themes/${encodeURIComponent(fileName)}/apply`, {
@@ -78,7 +82,10 @@ export async function applyTheme(fileName: string): Promise<ApplyThemeResult> {
78
82
  const err = await res.json().catch(() => ({ error: 'Apply failed' }));
79
83
  throw new Error(err.error || 'Apply failed');
80
84
  }
81
- return res.json();
85
+ const result = await res.json() as ApplyThemeResult;
86
+ hydrateAppliedTheme(fileName, result);
87
+ broadcastAppliedTheme(fileName, result);
88
+ return result;
82
89
  }
83
90
 
84
91
  export interface AdoptLookResult {
@@ -16,8 +16,17 @@ export interface PaletteConfig {
16
16
  baseColor: Oklch;
17
17
  lightnessCurve: CurveAnchor[];
18
18
  saturationCurve: CurveAnchor[];
19
- scaleCurves: Record<string, { lightness: CurveAnchor[]; saturation: CurveAnchor[] }>;
19
+ /** Signed hue offset in degrees per step, applied on top of the base hue.
20
+ * Absent means flat zero: the palette holds one hue, which is what every
21
+ * theme saved before this field did. */
22
+ hueCurve?: CurveAnchor[];
23
+ scaleCurves: Record<string, { lightness: CurveAnchor[]; saturation: CurveAnchor[]; hue?: CurveAnchor[] }>;
20
24
  curveOffset: Record<string, number>;
25
+ /** Per curve key: are the tangents derived from the points, or held by hand?
26
+ * Only an explicit switch-off is recorded. A missing key means the curve's own
27
+ * shape answers (`isAutoSmoothCurve`), which is what every theme saved before
28
+ * the flag existed relies on. */
29
+ curveAutoSmooth?: Record<string, boolean>;
21
30
  overrides: Record<string, Oklch>;
22
31
  snappedScales: string[];
23
32
  emptyMode?: 'solid' | 'gradient';
@@ -30,11 +39,12 @@ export interface PaletteConfig {
30
39
  /**
31
40
  * Where the base color sits in the ramp while `anchorToBase` is on: the
32
41
  * PALETTE_STEPS index whose curve anchors are pinned to the base color
33
- * (lightness = base L, saturation = multiplier 100). `displacedL` /
34
- * `displacedS` remember the y of a pre-existing anchor (endpoint or
35
- * user-authored) the placement overwrote at that x, so moving the
36
- * placement or toggling off restores it instead of deleting it.
37
- * `priorLightnessEndpoints` / `priorSaturationEndpoints` remember the two
42
+ * (lightness = base L, saturation = multiplier 100, hue delta = 0 when a
43
+ * hue curve exists). `displacedL` / `displacedS` / `displacedH` remember
44
+ * the y of a pre-existing anchor (endpoint or user-authored) the placement
45
+ * overwrote at that x, so moving the placement or toggling off restores it
46
+ * instead of deleting it. `priorLightnessEndpoints` /
47
+ * `priorSaturationEndpoints` / `priorHueEndpoints` remember the two
38
48
  * endpoints exactly as they stood before the curve's first-ever placement
39
49
  * (a smoothed reshape, not algebraically invertible the way a later
40
50
  * scaled-handle re-placement is), so a clear can still restore the true
@@ -44,8 +54,10 @@ export interface PaletteConfig {
44
54
  step: number;
45
55
  displacedL?: number;
46
56
  displacedS?: number;
57
+ displacedH?: number;
47
58
  priorLightnessEndpoints?: [CurveAnchor, CurveAnchor];
48
59
  priorSaturationEndpoints?: [CurveAnchor, CurveAnchor];
60
+ priorHueEndpoints?: [CurveAnchor, CurveAnchor];
49
61
  };
50
62
  /**
51
63
  * Set to true by importers when they overlay `cssVariables[--color-{ns}-*]`
@@ -20,8 +20,12 @@ Success, Warning, Info, Danger, and a few more) has:
20
20
 
21
21
  - **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)
22
22
  from it.
23
- - **Curves.** Two curves shape how lightness and saturation fall off across the
24
- ramp. Drag the handles to bias it darker, lighter, or more saturated.
23
+ - **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,
24
+ Lightness. Drag the handles to bias it warmer or cooler, more or less
25
+ saturated, darker or lighter. Hue drifts the ramp's temperature without
26
+ moving contrast, because OKLCH hue rotation is close to lightness-preserving.
27
+ It holds ±45 degrees; a bigger shift belongs on the base colour or the
28
+ harmony axis.
25
29
  - **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't
26
30
  land where you want.
27
31
 
@@ -30,7 +30,8 @@ A theme is a document, and the editor works the way any editor does.
30
30
  one slot per part of the look. **Save** captures that buffer into the open
31
31
  theme.
32
32
  - **The production theme** is the one your site ships, named in
33
- `themes/_production.json`. Only **Adopt** changes it.
33
+ `themes/_production.json`. **Adopt** changes it; saving a preset in the Theme
34
+ Picker performs that Adopt for you.
34
35
 
35
36
  Absence is the answer for anything untouched: a buffer exists only where the
36
37
  live look diverges from the active theme, so a newly opened theme has none.
@@ -43,9 +44,11 @@ In the Theme panel:
43
44
  go in as part of it, so there is nothing to save first.
44
45
  - **Save As** names a new theme. Use it for your first save and for forking.
45
46
 
46
- Component edits are the exception. Each component editor holds its own unsaved
47
- state, which this panel cannot write, so save a component in its editor before
48
- capturing it. The panel says how many are waiting.
47
+ Component editors keep their own unsaved state. If one or more components are
48
+ waiting when you use **Save**, **Save As**, or **Adopt**, the Theme panel offers
49
+ to save all of them before continuing. You can accept once instead of visiting
50
+ each component, or cancel to review them individually. A component editor's
51
+ **Save As** creates a reusable component preset.
49
52
 
50
53
  Names are tidied to lowercase with hyphens, so "My Brand!" becomes `my-brand`,
51
54
  and a leading underscore is dropped: those names are reserved for the buffer.
@@ -54,13 +57,13 @@ to it, and the editor never overwrites it, so start your own with **Save As**.
54
57
 
55
58
  ## Switching
56
59
 
57
- **Load** lists your saved themes and the seven example looks. Picking one shows
58
- it on the page as a preview with nothing written to disk, so you can try each
59
- look and compare. **Save** in that window opens the previewed theme: the active
60
- pointer changes, the buffers clear, components it does not carry fall through
61
- to their defaults, and the editor works on it from then on. **Cancel** returns
62
- you to where you were.
63
- Trying a look never changes what your site ships.
60
+ **Load**—or clicking the active theme's name—opens the Theme Picker. Picking a
61
+ theme shows it on the page as a preview with nothing written to disk, so you can
62
+ try each look and compare. **Save** in that window opens and adopts the previewed
63
+ theme in one step: the active pointer changes, the buffers clear, components it
64
+ does not carry fall through to their defaults, the editor works on it, and
65
+ production ships it. **Cancel** returns you to where you were. Previewing alone
66
+ never changes what your site ships.
64
67
 
65
68
  **Colors and type only. Keep my shapes.** narrows the load to the palette and
66
69
  the fonts: your component settings stay as they are and the theme you have open
@@ -34,8 +34,8 @@ deleting anything else never breaks it.
34
34
 
35
35
  - **Editing** changes the page through CSS variables. The editor keeps your
36
36
  edits in the browser as you work and writes them to the `_working.json`
37
- buffers when you save a component. A buffer exists only where the live layer
38
- differs from the active theme's saved layer, so a fresh project has none.
37
+ buffers when you save a component. When the Theme panel finds several dirty
38
+ components, **Save all** writes those buffers together.
39
39
  - **Save** captures the buffers into the open theme's file. That file is the
40
40
  durable copy of your look; matching buffers are then removed.
41
41
  - **Load** clears the buffers and points `themes/_active.json` at the theme you
@@ -4,8 +4,8 @@
4
4
  export const docContent: Record<string, string> = {
5
5
  "01-overview": "# Overview\n\nLive Tokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button,\n IconButton, Card, Dialog, Table, and more). You style components by changing\n the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
6
6
  "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
7
- "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has two views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Two curves shape how lightness and saturation fall off across the\n ramp. Drag the handles to bias it darker, lighter, or more saturated.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Overlays and gradients\n\n- **Overlays** are translucent tints layered over surfaces, like the subtle\n tint a card gets on hover. Set a colour and opacity per state.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
7
+ "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has two views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Overlays and gradients\n\n- **Overlays** are translucent tints layered over surfaces, like the subtle\n tint a card gets on hover. Set a colour and opacity per state.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
8
8
  "getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your\n theme appears as JSON under `src/live-tokens/data/themes/`.\n5. Reload. The editor reopens on your theme, so the page returns as you left\n it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, click **Adopt** in the Theme panel. That saves the open theme and bakes\nit into `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Adopt is the only action that changes what your site\nships, so try any look you like first. The editor itself never reaches\nproduction.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
9
- "themes-workflow": "# Themes\n\nSave your work, switch between looks, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole look:\ncolors, type, and a setting for every component you changed, in one file. It\ncarries the name the look ships under, whether production is running it, and\n**Adopt**. Two parts sit under it, each a read-out rather than a file to manage.\n\n- **Colors & Type** holds the design tokens. Components read those tokens to\n define their appearance. It names the two faces the page is showing.\n- **Components** counts how many components run something the theme does not\n carry, and opens the component editors.\n\nA theme holds its own copy of every part, so one theme can never break another.\n\n## How themes work\n\nA theme is a document, and the editor works the way any editor does.\n\n- **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries\n the whole look: the colors and type plus a setting for every component you\n changed.\n- **The open theme** is the one the editor is working on, named in\n `themes/_active.json`. One at a time.\n- **Your unsaved edits** are what the page shows right now. The editor keeps\n them in your browser as you work and writes them to a buffer, `_working.json`,\n one slot per part of the look. **Save** captures that buffer into the open\n theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`. Only **Adopt** changes it.\n\nAbsence is the answer for anything untouched: a buffer exists only where the\nlive look diverges from the active theme, so a newly opened theme has none.\n\n## Saving\n\nIn the Theme panel:\n\n- **Save** captures the look on screen into the open theme. Your colors and type\n go in as part of it, so there is nothing to save first.\n- **Save As** names a new theme. Use it for your first save and for forking.\n\nComponent edits are the exception. Each component editor holds its own unsaved\nstate, which this panel cannot write, so save a component in its editor before\ncapturing it. The panel says how many are waiting.\n\nNames are tidied to lowercase with hyphens, so \"My Brand!\" becomes `my-brand`,\nand a leading underscore is dropped: those names are reserved for the buffer.\n**Motion Proto** is the built-in theme and is read-only. You can always return\nto it, and the editor never overwrites it, so start your own with **Save As**.\n\n## Switching\n\n**Load** lists your saved themes and the seven example looks. Picking one shows\nit on the page as a preview with nothing written to disk, so you can try each\nlook and compare. **Save** in that window opens the previewed theme: the active\npointer changes, the buffers clear, components it does not carry fall through\nto their defaults, and the editor works on it from then on. **Cancel** returns\nyou to where you were.\nTrying a look never changes what your site ships.\n\n**Colors and type only. Keep my shapes.** narrows the load to the palette and\nthe fonts: your component settings stay as they are and the theme you have open\nstays open. Saved colors and type files are listed there too, marked *colors &\ntype*, and picking one is always that narrower load.\n\n## Shipping\n\n**Adopt**, in the Theme panel, is the \"ship it\" step, and it ships the whole\nlook. It saves the open theme, then bakes that theme into\n`src/live-tokens/data/tokens.generated.css`, which your build bundles alongside\n`tokens.css`: the colors and type plus every component the theme carries. Fonts\nregenerate to match. The line under the theme name says whether production is\nrunning this theme.\n\nProduction is one saved theme, so nothing else publishes. Trying a look, moving\na token, saving a theme: all of it leaves the generated CSS alone until you\nAdopt. A component editor's Adopt runs the same whole-look step, because a\ncomponent never ships alone. Adopting while Motion Proto is open saves your look\nas a theme of your own first, since the built-in one is read-only.\n\nProduction builds (`npm run build`) ship only that plain CSS and your\ncomponents. No editor, no JSON loading, no runtime indirection.\n\n## Keeping your work safe\n\nEverything under `src/live-tokens/data/` is plain JSON, so commit it. Themes show\nup as readable diffs you can review per branch, and the buffer shows up as the\nwork you have not saved into a theme yet. Nothing is backed up anywhere else:\ngit is your safety net. To experiment freely, **Save As** a new name first, then\nedit.\n\n## Where to go next\n\n- **[Where themes live](where-themes-live.md)**: the files behind all of this,\n and what writes each one.\n- **[Creating components](creating-components.md)**: make your own components\n editable in the same editor.\n",
10
- "where-themes-live": "# Where themes live\n\nEverything the editor writes is plain JSON and CSS inside your project. There\nis no database and no hidden state: the files are the storage, and git is the\nhistory.\n\n## The data tree\n\n```\nsrc/live-tokens/data/\n themes/\n _active.json names the theme the editor has open\n _production.json names the theme your site ships\n default.json Motion Proto, the built-in look, rewritten at boot\n my-brand.json a saved theme: the whole look in one file\n colors-and-type/\n _working.json unsaved colors and type edits\n component-configs/\n button/\n default.json Button's shipped settings, derived at boot\n _working.json unsaved Button edits\n my-button.json a preset you saved from the Button editor\n tokens.generated.css the baked CSS your production build ships\nsrc/system/styles/\n tokens.css your token vocabulary, hand-authored, never written\n fonts.css font imports, rewritten when you Adopt\n```\n\nA saved theme carries the whole look by value: the colors and type plus a\nsetting for every component you changed. It depends on no other file, so\ndeleting anything else never breaks it.\n\n## What writes when\n\n- **Editing** changes the page through CSS variables. The editor keeps your\n edits in the browser as you work and writes them to the `_working.json`\n buffers when you save a component. A buffer exists only where the live layer\n differs from the active theme's saved layer, so a fresh project has none.\n- **Save** captures the buffers into the open theme's file. That file is the\n durable copy of your look; matching buffers are then removed.\n- **Load** clears the buffers and points `themes/_active.json` at the theme you\n picked. Live reads fall through to that file. Nothing else changes, so trying\n looks is free and ordinary switching changes only the pointer.\n- **Adopt** points `themes/_production.json` at the open theme, bakes it into\n `tokens.generated.css`, and rewrites `fonts.css` to match. It is the only\n action that changes what your site ships.\n\nThe `default.json` files are the shipped baseline. The editor derives them at\nboot and refreshes them when the package updates; it never saves your work\nover them.\n\nProjects upgraded from 0.48 may initially contain working files copied from the\nactive theme. On the first dev-server boot, exact copies are removed\nautomatically. Any file that differs is kept as unsaved work, so no migration\ncommand is required.\n\n## What to commit\n\nAll of it. The data tree is designed to live in git: themes diff readably, the\ntwo pointers say what is open and what ships, and a `_working.json` in a diff\nis exactly the work you have not yet saved into a theme. Nothing is backed up\nanywhere else.\n\n## Where to go next\n\n- **[Themes](themes-workflow.md)**: the workflow built on these files: saving,\n loading, and shipping.\n",
9
+ "themes-workflow": "# Themes\n\nSave your work, switch between looks, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole look:\ncolors, type, and a setting for every component you changed, in one file. It\ncarries the name the look ships under, whether production is running it, and\n**Adopt**. Two parts sit under it, each a read-out rather than a file to manage.\n\n- **Colors & Type** holds the design tokens. Components read those tokens to\n define their appearance. It names the two faces the page is showing.\n- **Components** counts how many components run something the theme does not\n carry, and opens the component editors.\n\nA theme holds its own copy of every part, so one theme can never break another.\n\n## How themes work\n\nA theme is a document, and the editor works the way any editor does.\n\n- **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries\n the whole look: the colors and type plus a setting for every component you\n changed.\n- **The open theme** is the one the editor is working on, named in\n `themes/_active.json`. One at a time.\n- **Your unsaved edits** are what the page shows right now. The editor keeps\n them in your browser as you work and writes them to a buffer, `_working.json`,\n one slot per part of the look. **Save** captures that buffer into the open\n theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`. **Adopt** changes it; saving a preset in the Theme\n Picker performs that Adopt for you.\n\nAbsence is the answer for anything untouched: a buffer exists only where the\nlive look diverges from the active theme, so a newly opened theme has none.\n\n## Saving\n\nIn the Theme panel:\n\n- **Save** captures the look on screen into the open theme. Your colors and type\n go in as part of it, so there is nothing to save first.\n- **Save As** names a new theme. Use it for your first save and for forking.\n\nComponent editors keep their own unsaved state. If one or more components are\nwaiting when you use **Save**, **Save As**, or **Adopt**, the Theme panel offers\nto save all of them before continuing. You can accept once instead of visiting\neach component, or cancel to review them individually. A component editor's\n**Save As** creates a reusable component preset.\n\nNames are tidied to lowercase with hyphens, so \"My Brand!\" becomes `my-brand`,\nand a leading underscore is dropped: those names are reserved for the buffer.\n**Motion Proto** is the built-in theme and is read-only. You can always return\nto it, and the editor never overwrites it, so start your own with **Save As**.\n\n## Switching\n\n**Load**—or clicking the active theme's name—opens the Theme Picker. Picking a\ntheme shows it on the page as a preview with nothing written to disk, so you can\ntry each look and compare. **Save** in that window opens and adopts the previewed\ntheme in one step: the active pointer changes, the buffers clear, components it\ndoes not carry fall through to their defaults, the editor works on it, and\nproduction ships it. **Cancel** returns you to where you were. Previewing alone\nnever changes what your site ships.\n\n**Colors and type only. Keep my shapes.** narrows the load to the palette and\nthe fonts: your component settings stay as they are and the theme you have open\nstays open. Saved colors and type files are listed there too, marked *colors &\ntype*, and picking one is always that narrower load.\n\n## Shipping\n\n**Adopt**, in the Theme panel, is the \"ship it\" step, and it ships the whole\nlook. It saves the open theme, then bakes that theme into\n`src/live-tokens/data/tokens.generated.css`, which your build bundles alongside\n`tokens.css`: the colors and type plus every component the theme carries. Fonts\nregenerate to match. The line under the theme name says whether production is\nrunning this theme.\n\nProduction is one saved theme, so nothing else publishes. Trying a look, moving\na token, saving a theme: all of it leaves the generated CSS alone until you\nAdopt. A component editor's Adopt runs the same whole-look step, because a\ncomponent never ships alone. Adopting while Motion Proto is open saves your look\nas a theme of your own first, since the built-in one is read-only.\n\nProduction builds (`npm run build`) ship only that plain CSS and your\ncomponents. No editor, no JSON loading, no runtime indirection.\n\n## Keeping your work safe\n\nEverything under `src/live-tokens/data/` is plain JSON, so commit it. Themes show\nup as readable diffs you can review per branch, and the buffer shows up as the\nwork you have not saved into a theme yet. Nothing is backed up anywhere else:\ngit is your safety net. To experiment freely, **Save As** a new name first, then\nedit.\n\n## Where to go next\n\n- **[Where themes live](where-themes-live.md)**: the files behind all of this,\n and what writes each one.\n- **[Creating components](creating-components.md)**: make your own components\n editable in the same editor.\n",
10
+ "where-themes-live": "# Where themes live\n\nEverything the editor writes is plain JSON and CSS inside your project. There\nis no database and no hidden state: the files are the storage, and git is the\nhistory.\n\n## The data tree\n\n```\nsrc/live-tokens/data/\n themes/\n _active.json names the theme the editor has open\n _production.json names the theme your site ships\n default.json Motion Proto, the built-in look, rewritten at boot\n my-brand.json a saved theme: the whole look in one file\n colors-and-type/\n _working.json unsaved colors and type edits\n component-configs/\n button/\n default.json Button's shipped settings, derived at boot\n _working.json unsaved Button edits\n my-button.json a preset you saved from the Button editor\n tokens.generated.css the baked CSS your production build ships\nsrc/system/styles/\n tokens.css your token vocabulary, hand-authored, never written\n fonts.css font imports, rewritten when you Adopt\n```\n\nA saved theme carries the whole look by value: the colors and type plus a\nsetting for every component you changed. It depends on no other file, so\ndeleting anything else never breaks it.\n\n## What writes when\n\n- **Editing** changes the page through CSS variables. The editor keeps your\n edits in the browser as you work and writes them to the `_working.json`\n buffers when you save a component. When the Theme panel finds several dirty\n components, **Save all** writes those buffers together.\n- **Save** captures the buffers into the open theme's file. That file is the\n durable copy of your look; matching buffers are then removed.\n- **Load** clears the buffers and points `themes/_active.json` at the theme you\n picked. Live reads fall through to that file. Nothing else changes, so trying\n looks is free and ordinary switching changes only the pointer.\n- **Adopt** points `themes/_production.json` at the open theme, bakes it into\n `tokens.generated.css`, and rewrites `fonts.css` to match. It is the only\n action that changes what your site ships.\n\nThe `default.json` files are the shipped baseline. The editor derives them at\nboot and refreshes them when the package updates; it never saves your work\nover them.\n\nProjects upgraded from 0.48 may initially contain working files copied from the\nactive theme. On the first dev-server boot, exact copies are removed\nautomatically. Any file that differs is kept as unsaved work, so no migration\ncommand is required.\n\n## What to commit\n\nAll of it. The data tree is designed to live in git: themes diff readably, the\ntwo pointers say what is open and what ships, and a `_working.json` in a diff\nis exactly the work you have not yet saved into a theme. Nothing is backed up\nanywhere else.\n\n## Where to go next\n\n- **[Themes](themes-workflow.md)**: the workflow built on these files: saving,\n loading, and shipping.\n",
11
11
  };