@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
@@ -1,102 +1,242 @@
1
- import { tick } from 'svelte';
2
- import type { Theme, ThemeMeta } from './themeTypes';
3
- import type { EditorState } from '../store/editorTypes';
4
- import {
5
- versionedFileResource,
6
- sanitizeFileName as sanitizeFileNameImpl,
7
- } from '../storage/files/versionedFileResourceClient';
1
+ import type { Theme, ThemeMeta, ThemeBundle, ColorsAndType, ComponentConfig } from './themeTypes';
2
+ import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
8
3
  import { API_BASE } from '../storage/apiBase';
9
- import { loadFromFile as loadEditorState, toTheme, markSaved } from '../store/editorStore';
10
- import { activeFileName } from '../store/editorConfigStore';
11
- import { applyFontSources, applyFontStacks } from '../fonts/fontLoader';
12
- import { migrateThemeFonts } from '../fonts/fontMigration';
13
-
14
- // ── API helpers ──────────────────────────────────────────────
15
- //
16
- // All theme CRUD goes through `versionedFileResource(`${API_BASE}/themes`)` —
17
- // shared with `componentConfigService`'s per-component clients. Theme-specific
18
- // response shapes (ThemeMeta list payload, ProductionInfo) are layered on top
19
- // via the generic type parameters.
20
-
21
- export interface ProductionInfo {
22
- fileName: string;
23
- name: string;
24
- updatedAt: string;
25
- cssVariables: Record<string, string>;
26
- }
4
+ import { liveMovedSinceBake } from '../productionPulse';
5
+ import { listComponents, getActiveComponentConfig } from '../components/componentConfigService';
6
+ import { getActiveColorsAndType } from './colorsAndTypeService';
7
+
8
+ /**
9
+ * REST client for theme files, the documents of the editor. A theme carries a
10
+ * whole look by value: the colors and type plus a config for every component
11
+ * that is off its default. One theme is open (`themes/_active.json`) and one is
12
+ * published (`themes/_production.json`); the live look is the open theme plus
13
+ * whatever the `_working` buffers hold over it.
14
+ *
15
+ * `default` is the protected baseline — cannot be overwritten or deleted, and
16
+ * Adopt returns 409 ACTIVE_IS_PROTECTED while it is open.
17
+ */
27
18
 
28
- const themeResource = versionedFileResource<Theme, ThemeMeta, ProductionInfo>({
19
+ const themesResource = versionedFileResource<Theme, ThemeMeta>({
29
20
  baseUrl: `${API_BASE}/themes`,
30
21
  });
31
22
 
32
- export async function listThemes(): Promise<ThemeMeta[]> {
33
- const data = await themeResource.list();
23
+ export const listThemes = async (): Promise<ThemeMeta[]> => {
24
+ const data = await themesResource.list();
34
25
  return data.files;
35
- }
26
+ };
36
27
 
37
- export const loadTheme = (fileName: string): Promise<Theme> => themeResource.load(fileName);
28
+ export const loadTheme = (fileName: string): Promise<Theme> =>
29
+ themesResource.load(fileName);
38
30
  export const saveTheme = (fileName: string, data: Theme): Promise<void> =>
39
- themeResource.save(fileName, data);
40
- export const deleteTheme = (fileName: string): Promise<void> => themeResource.remove(fileName);
31
+ themesResource.save(fileName, data);
32
+ export const deleteTheme = (fileName: string): Promise<void> =>
33
+ themesResource.remove(fileName);
34
+ export const getActiveTheme = (): Promise<Theme | null> => themesResource.getActive();
35
+ export const setActiveTheme = (fileName: string): Promise<void> =>
36
+ themesResource.setActive(fileName);
41
37
 
42
- export async function getActiveTheme(): Promise<Theme | null> {
43
- return themeResource.getActive();
38
+ /** The published theme, served whole so the client can tell what production
39
+ * runs from the document itself. */
40
+ export async function getProductionTheme(): Promise<Theme> {
41
+ const res = await fetch(`${API_BASE}/themes/production`);
42
+ if (!res.ok) throw new Error('Failed to read the production theme');
43
+ return res.json();
44
44
  }
45
45
 
46
- export const setActiveFile = (fileName: string): Promise<void> => themeResource.setActive(fileName);
46
+ /** Step past the theme names already on disk rather than clobbering one. */
47
+ export function freshName(base: string, taken: Set<string>): string {
48
+ if (!taken.has(base)) return base;
49
+ for (let n = 1; n < 1000; n++) {
50
+ const candidate = `${base}_${String(n).padStart(2, '0')}`;
51
+ if (!taken.has(candidate)) return candidate;
52
+ }
53
+ return `${base}_${Date.now()}`;
54
+ }
47
55
 
48
- // ── Production API helpers ─────────────────────────────────
56
+ export interface ApplyThemeResult {
57
+ ok: boolean;
58
+ theme: Theme;
59
+ colorsAndType: ColorsAndType;
60
+ componentConfigs: Record<string, ComponentConfig>;
61
+ /** Components the theme carries data for that this install doesn't have. */
62
+ skippedComponents: string[];
63
+ }
49
64
 
50
- export const getProductionInfo = (): Promise<ProductionInfo> => themeResource.getProductionInfo();
65
+ /**
66
+ * Open a theme: the server writes its embedded copies into the `_working`
67
+ * buffers, clears the buffer of every component it does not carry, points
68
+ * `themes/_active.json` at it and returns the resolved state in one payload.
69
+ * 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.
72
+ */
73
+ export async function applyTheme(fileName: string): Promise<ApplyThemeResult> {
74
+ const res = await fetch(`${API_BASE}/themes/${encodeURIComponent(fileName)}/apply`, {
75
+ method: 'PUT',
76
+ });
77
+ if (!res.ok) {
78
+ const err = await res.json().catch(() => ({ error: 'Apply failed' }));
79
+ throw new Error(err.error || 'Apply failed');
80
+ }
81
+ return res.json();
82
+ }
51
83
 
52
- export async function setProductionFile(
53
- fileName: string,
54
- ): Promise<{ ok: boolean; fileName: string; name: string }> {
55
- const data = await themeResource.setProduction(fileName);
56
- return { ok: data.ok, fileName: data.fileName, name: data.name };
84
+ export interface AdoptLookResult {
85
+ ok: boolean;
86
+ /** The theme production now ships. */
87
+ productionTheme: { fileName: string; name: string; updatedAt: string };
88
+ }
89
+
90
+ /**
91
+ * Publish the open theme: `themes/_production.json` names it and
92
+ * `tokens.generated.css` plus `fonts.css` are rebaked from its embedded
93
+ * content. What is SAVED ships, so the caller saves the theme first; a buffer
94
+ * the server has not been handed is invisible from here.
95
+ *
96
+ * Answers 409 `ACTIVE_IS_PROTECTED` while the Default theme is open, which the
97
+ * caller recovers from by forking it under a name of the user's own.
98
+ */
99
+ export async function adoptLook(): Promise<AdoptLookResult> {
100
+ const res = await fetch(`${API_BASE}/production`, { method: 'PUT' });
101
+ if (!res.ok) {
102
+ const body = await res.json().catch(() => ({}));
103
+ const err = new Error(body.error || 'Adopt failed') as Error & {
104
+ status?: number;
105
+ code?: string;
106
+ };
107
+ err.status = res.status;
108
+ if (body.code) err.code = body.code;
109
+ throw err;
110
+ }
111
+ liveMovedSinceBake.set(false);
112
+ return res.json();
57
113
  }
58
114
 
59
- /** Sanitize a display name to a safe file name. Re-exported from the shared
60
- * `files/versionedFileResource` so the dev-server plugin can import the
61
- * canonical pure helper without depending on this module's CSS imports. */
62
- export const sanitizeFileName = sanitizeFileNameImpl;
63
-
64
- // ── Theme save/load orchestration ──────────────────────────
65
- //
66
- // `persistTheme` and `hydrateTheme` are the canonical entry points for
67
- // round-tripping editor state to disk. Callers (e.g. `EditorShell`) need
68
- // only handle UI-level concerns (status flashing, error chrome) and
69
- // delegate the actual orchestration here.
70
-
71
- /** Snapshot the editor state to disk under `fileName`, mark the file active,
72
- * and clear the dirty flag. The caller is responsible for surfacing
73
- * saving / saved / error UI states around this call. */
74
- export async function persistTheme(
75
- state: EditorState,
115
+ /** `_fileName` and `_source` mark which document and which layer a read door
116
+ * answered from; neither is part of the content we send back. */
117
+ function withoutLiveMarkers<T extends { _fileName?: string; _source?: unknown }>(value: T): T {
118
+ const { _fileName, _source, ...rest } = value;
119
+ return rest as T;
120
+ }
121
+
122
+ /**
123
+ * The look as it stands: the live colors and type plus the live config of
124
+ * every component that sits off its default, all by value. Delta encoding — a
125
+ * component absent from the map runs the local default, which stays canonical.
126
+ *
127
+ * Everything comes from the live read doors, so a capture takes the buffers
128
+ * over the open theme's own copies. The colors and type are normalised on the
129
+ * way out of `GET /colors-and-type/active`, which matters: the server trusts an
130
+ * already-embedded copy and runs no migrations over it on write.
131
+ */
132
+ async function captureLook(): Promise<Pick<Theme, 'colorsAndType' | 'componentConfigs'>> {
133
+ const liveColorsAndType = await getActiveColorsAndType();
134
+ if (!liveColorsAndType) {
135
+ throw new Error('No live colors and type to capture');
136
+ }
137
+ const overridden = (await listComponents()).filter((c) => c.source !== 'default');
138
+ const configs = await Promise.all(overridden.map((c) => getActiveComponentConfig(c.name)));
139
+ const componentConfigs: Record<string, ComponentConfig> = {};
140
+ overridden.forEach((c, i) => {
141
+ const config = configs[i];
142
+ if (config) componentConfigs[c.name] = withoutLiveMarkers(config);
143
+ });
144
+ return { colorsAndType: withoutLiveMarkers(liveColorsAndType), componentConfigs };
145
+ }
146
+
147
+ /**
148
+ * Capture the current look into a new theme file and open it. Used by the
149
+ * theme panel's Save As action and by the fork-then-Adopt flow the protected
150
+ * Default theme forces.
151
+ */
152
+ export async function saveAsTheme(
76
153
  fileName: string,
77
154
  displayName: string,
78
155
  ): Promise<void> {
79
- await tick();
80
- const theme = toTheme(state, { name: displayName });
81
- await saveTheme(fileName, theme);
82
- await setActiveFile(fileName);
83
- activeFileName.set(fileName);
84
- markSaved();
156
+ const look = await captureLook();
157
+ const now = new Date().toISOString();
158
+ await saveTheme(fileName, {
159
+ name: displayName,
160
+ createdAt: now,
161
+ updatedAt: now,
162
+ schemaVersion: 3,
163
+ ...look,
164
+ });
165
+ liveMovedSinceBake.set(true);
166
+ await setActiveTheme(fileName);
167
+ }
168
+
169
+ /**
170
+ * Re-capture the current look into the open theme file. Used by the theme
171
+ * panel's Save action and by both Adopt paths, which publish what is saved.
172
+ * The server rejects with 403 while the protected `default` theme is open.
173
+ */
174
+ export async function saveActiveTheme(displayName?: string): Promise<void> {
175
+ const active = await getActiveTheme();
176
+ if (!active || !active._fileName) {
177
+ throw new Error('No active theme');
178
+ }
179
+ const look = await captureLook();
180
+ await saveTheme(active._fileName, {
181
+ name: displayName ?? active.name,
182
+ createdAt: active.createdAt,
183
+ updatedAt: new Date().toISOString(),
184
+ schemaVersion: 3,
185
+ ...look,
186
+ });
187
+ liveMovedSinceBake.set(true);
188
+ }
189
+
190
+ export interface ImportThemeResult {
191
+ ok: boolean;
192
+ /** Final theme filename (may be renamed if it collided with an existing one). */
193
+ theme: string;
194
+ /** Keyed `theme:<orig>` → final name. */
195
+ renames: Record<string, string>;
196
+ /** Refs a v1 bundle failed to carry, as `colors-and-type:<name>` /
197
+ * `<comp>/<name>`. Each fell back to the default. */
198
+ dropped: string[];
85
199
  }
86
200
 
87
- /** Load a theme file into the editor state and re-apply font side-effects
88
- * (@font-face rules + `--font-*` CSS vars on :root). */
89
- export async function hydrateTheme(fileName: string): Promise<void> {
90
- const theme = await loadTheme(fileName);
91
- migrateThemeFonts(theme);
92
- loadEditorState(theme);
93
- // Font data is in state.fonts via loadEditorState; the DOM-side-effect
94
- // helpers still need to run so @font-face rules and --font-* CSS vars
95
- // land on :root.
96
- if (theme.fontSources && theme.fontSources.length > 0) {
97
- applyFontSources(theme.fontSources);
201
+ /**
202
+ * Fetch the theme as a `ThemeBundle` and trigger a browser download.
203
+ * Hidden-anchor trick — no infrastructure beyond the existing GET
204
+ * `/api/themes/:name/export` endpoint.
205
+ */
206
+ export async function exportTheme(fileName: string): Promise<void> {
207
+ const res = await fetch(`${API_BASE}/themes/${encodeURIComponent(fileName)}/export`);
208
+ if (!res.ok) {
209
+ const err = await res.json().catch(() => ({ error: 'Export failed' }));
210
+ throw new Error(err.error || 'Export failed');
98
211
  }
99
- if (theme.fontStacks && theme.fontStacks.length > 0) {
100
- applyFontStacks(theme.fontStacks, theme.fontSources ?? []);
212
+ const blob = await res.blob();
213
+ const url = URL.createObjectURL(blob);
214
+ try {
215
+ const a = document.createElement('a');
216
+ a.href = url;
217
+ a.download = `${fileName}.bundle.json`;
218
+ document.body.appendChild(a);
219
+ a.click();
220
+ a.remove();
221
+ } finally {
222
+ URL.revokeObjectURL(url);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * POST a `ThemeBundle` to the import endpoint. The server writes one
228
+ * theme file (renaming on collision) and returns its final name; nothing is
229
+ * materialised until Apply. v1 bundles from older installs are accepted too.
230
+ */
231
+ export async function importTheme(bundle: ThemeBundle): Promise<ImportThemeResult> {
232
+ const res = await fetch(`${API_BASE}/themes/import`, {
233
+ method: 'POST',
234
+ headers: { 'Content-Type': 'application/json' },
235
+ body: JSON.stringify(bundle),
236
+ });
237
+ if (!res.ok) {
238
+ const err = await res.json().catch(() => ({ error: 'Import failed' }));
239
+ throw new Error(err.error || 'Import failed');
101
240
  }
241
+ return res.json();
102
242
  }
@@ -34,8 +34,19 @@ export interface PaletteConfig {
34
34
  * `displacedS` remember the y of a pre-existing anchor (endpoint or
35
35
  * user-authored) the placement overwrote at that x, so moving the
36
36
  * placement or toggling off restores it instead of deleting it.
37
+ * `priorLightnessEndpoints` / `priorSaturationEndpoints` remember the two
38
+ * endpoints exactly as they stood before the curve's first-ever placement
39
+ * (a smoothed reshape, not algebraically invertible the way a later
40
+ * scaled-handle re-placement is), so a clear can still restore the true
41
+ * original regardless of how many times the anchor has since moved.
37
42
  */
38
- anchorPlacement?: { step: number; displacedL?: number; displacedS?: number };
43
+ anchorPlacement?: {
44
+ step: number;
45
+ displacedL?: number;
46
+ displacedS?: number;
47
+ priorLightnessEndpoints?: [CurveAnchor, CurveAnchor];
48
+ priorSaturationEndpoints?: [CurveAnchor, CurveAnchor];
49
+ };
39
50
  /**
40
51
  * Set to true by importers when they overlay `cssVariables[--color-{ns}-*]`
41
52
  * without owning the typed-state curves. The storage-layer reconciler uses
@@ -83,7 +94,31 @@ export interface FontStack {
83
94
  slots: FontStackSlot[];
84
95
  }
85
96
 
86
- export interface Theme {
97
+ /** On-disk shape of one `--gradient-N` library token. Structural mirror of the
98
+ * store's `GradientToken` (like `AliasDiskValue`'s inline gradient shape) so
99
+ * the file schema stays independent of store types. The rendered strings in
100
+ * `cssVariables` are a projection of these for production CSS; this field is
101
+ * the editable basis. */
102
+ export interface GradientDiskToken {
103
+ variable: string;
104
+ type: 'linear' | 'radial' | 'solid' | 'none';
105
+ angle: number;
106
+ radius?: number;
107
+ centerX?: number;
108
+ aspectX?: number;
109
+ aspectY?: number;
110
+ stops: { position: number; color: string; opacity?: number }[];
111
+ }
112
+
113
+ /**
114
+ * Where a live read resolved from: the unsaved `_working` buffer, the open
115
+ * theme's embedded copy, or the shipped default. It says which layer answered,
116
+ * never whether the content was edited — applying a theme fills the buffer for
117
+ * every layer it carries.
118
+ */
119
+ export type LiveSource = 'working' | 'theme' | 'default';
120
+
121
+ export interface ColorsAndType {
87
122
  name: string;
88
123
  createdAt: string;
89
124
  updatedAt: string;
@@ -91,14 +126,19 @@ export interface Theme {
91
126
  cssVariables: Record<string, string>;
92
127
  fontSources?: FontSource[];
93
128
  fontStacks?: FontStack[];
129
+ /** Absent on files saved before gradients round-tripped; the loader keeps
130
+ * the stock defaults then, which match what those files rendered. */
131
+ gradients?: GradientDiskToken[];
94
132
  /** Four fixed harmony axes, each owning a hue with an optional bound family. */
95
133
  harmonyAxes?: HarmonyAxis[];
96
134
  /**
97
- * Server-attached file-name marker for round-tripping the file identity
98
- * back to the client. Set by `themeFileApi`'s GET handlers; read by
99
- * `themeInit` to seed `activeFileName`. Optional and not persisted to disk.
135
+ * Server-attached marker naming the open theme — the document this content
136
+ * belongs to, not a colors-and-type file. Set by `themeFileApi`'s GET
137
+ * handlers; read by `themeInit` to seed `openThemeSlug`. Not persisted.
100
138
  */
101
139
  _fileName?: string;
140
+ /** Server-attached marker: which of the three live layers answered. */
141
+ _source?: LiveSource;
102
142
  /**
103
143
  * Migration stamp. Absent on legacy files, treated as 0; the loader runs
104
144
  * any registered theme migrations whose `fromVersion >= file.schemaVersion`.
@@ -108,11 +148,13 @@ export interface Theme {
108
148
  schemaVersion?: number;
109
149
  }
110
150
 
111
- export interface ThemeMeta {
151
+ export interface ColorsAndTypeMeta {
112
152
  name: string;
113
153
  fileName: string;
114
154
  updatedAt: string;
115
- isActive: boolean;
155
+ /** The file is served from the installed package and no local copy shadows
156
+ * it: one of the shipped presets or the default, not a file the user made. */
157
+ isPackage: boolean;
116
158
  }
117
159
 
118
160
  /** On-disk shape of a single alias entry. Plain strings carry the bulk of
@@ -131,12 +173,14 @@ export interface ComponentConfig {
131
173
  aliases: Record<string, AliasDiskValue>;
132
174
  config?: Record<string, unknown>;
133
175
  /**
134
- * Server-attached file-name marker. Same role as `Theme._fileName`. Set by
135
- * the component-configs GET handlers; not persisted to disk.
176
+ * Server-attached marker. Same role as `ColorsAndType._fileName`: the open
177
+ * theme, not a config file. Set by the component-configs GET handlers.
136
178
  */
137
179
  _fileName?: string;
180
+ /** Server-attached marker: which of the three live layers answered. */
181
+ _source?: LiveSource;
138
182
  /**
139
- * Migration stamp. Absent on legacy files, treated as 0. See `Theme.schemaVersion`.
183
+ * Migration stamp. Absent on legacy files, treated as 0. See `ColorsAndType.schemaVersion`.
140
184
  */
141
185
  schemaVersion?: number;
142
186
  }
@@ -145,71 +189,70 @@ export interface ComponentConfigMeta {
145
189
  name: string;
146
190
  fileName: string;
147
191
  updatedAt: string;
148
- isActive: boolean;
149
- isProduction: boolean;
150
192
  }
151
193
 
152
194
  /**
153
- * Manifest that captures an entire site state — the active theme plus the
154
- * active config for every component. Loading a manifest flips the relevant
155
- * `_active.json` pointers; the underlying theme + component-config files stay
156
- * the source of truth, so editing them flows through any manifest that
157
- * references them. The currently-active manifest is the live snapshot: theme
158
- * and component Adopts auto-patch its refs on the server.
195
+ * A saved look, encapsulated: the colors and type plus a config for every
196
+ * component that sits off its default, all carried by value. Themes are the
197
+ * documents of the editor. Applying one opens it: its embedded copies land in
198
+ * the reserved `_working` buffers and `themes/_active.json` names it. Saving
199
+ * captures the live buffers back into it; Adopt publishes it, and only then is
200
+ * `tokens.generated.css` rebaked.
159
201
  */
160
- export interface Manifest {
202
+ export interface Theme {
161
203
  name: string;
162
204
  createdAt: string;
163
205
  updatedAt: string;
164
- /** File basename (no `.json`) of the theme this manifest pins. */
165
- theme: string;
166
- /** Map of componentId → config file basename. Components omitted here fall
167
- * back to "default" at apply time. */
168
- componentConfigs: Record<string, string>;
169
- /** Server-attached file-name marker. Same role as `Theme._fileName`. */
206
+ /** Migration stamp. 1 was the pointer form (colors-and-type + config
207
+ * basenames); 2 encapsulated it under a `theme` key; 3 spells that key
208
+ * `colorsAndType`. The server rewrites older files at boot. */
209
+ schemaVersion: 3;
210
+ /** Full colors-and-type content. */
211
+ colorsAndType: ColorsAndType;
212
+ /** Component id → its config. Delta encoding: a component absent here is on
213
+ * its default. Defaults are never inlined — the local `default.json` derived
214
+ * from the component source is canonical, and a frozen copy would drift. */
215
+ componentConfigs: Record<string, ComponentConfig>;
216
+ /** Server-attached file-name marker. Same role as `ColorsAndType._fileName`. */
170
217
  _fileName?: string;
171
218
  }
172
219
 
173
220
  /**
174
- * Transport artifact for sharing a manifest with someone else. Self-contained:
175
- * the bundle inlines the referenced theme and every non-default component
176
- * config so the receiver doesn't need anything else on disk to apply it.
221
+ * Transport envelope for sharing a theme. The theme already carries
222
+ * everything needed to apply it, so the envelope adds only provenance.
177
223
  *
178
- * Bundles are *not* stored under `manifests/` — they're transient downloads /
179
- * uploads. Local manifests stay lightweight pointer files; bundles are the
180
- * import/export envelope. See temp/manifest-robustness-plan.md §11.
181
- *
182
- * `componentConfigs` is keyed by `${component}/${configName}` so a single map
183
- * carries multiple components. Entries whose manifest value is `"default"`
184
- * are deliberately omitted — the receiver's local `default.json` is the
185
- * live-tokens package's canonical default, and shipping the sender's default
186
- * would risk version-divergence with no clean conflict story.
224
+ * Bundles are *not* stored under `themes/` — they're transient downloads /
225
+ * uploads; import writes the enclosed theme as a single file.
187
226
  */
188
- export interface ManifestBundle {
189
- /** Discriminator for safe identification of bundle JSON files. */
190
- kind: 'manifest-bundle';
191
- /** Bumps when the bundle envelope shape changes. Start at 1. */
192
- schemaVersion: 1;
227
+ export interface ThemeBundle {
228
+ /** Discriminator for safe identification of bundle JSON files. Paired with
229
+ * the version below: import accepts this kind only at v3, and the
230
+ * `manifest-bundle` every release through 0.47.1 wrote only at v1. */
231
+ kind: 'theme-bundle';
232
+ /** Tracks the enclosed theme's schema. Import still accepts 1, where the
233
+ * envelope carried a pointer theme plus separately inlined colors and type
234
+ * and `${component}/${configName}`-keyed configs. */
235
+ schemaVersion: 3;
193
236
  /** Sender's `@motion-proto/live-tokens` package version. Receiver can
194
237
  * compare to its own to warn about compatibility drift. */
195
238
  liveTokensVersion: string;
196
239
  /** ISO timestamp of when the bundle was exported. */
197
240
  exportedAt: string;
198
- /** Full pointer-form manifest (same shape as on-disk manifest files). */
199
- manifest: Manifest;
200
- /** Full content of the theme that `manifest.theme` references. */
201
- theme: Theme;
202
- /** Full content of each non-default component config referenced by
203
- * `manifest.componentConfigs`, keyed by `${component}/${configName}`. */
204
- componentConfigs: Record<string, ComponentConfig>;
241
+ /** Keeps the `manifest` spelling: a v1 bundle's `theme` is the separately
242
+ * inlined colors and type, which import still reads, so the envelope cannot
243
+ * take that name without meaning two things at once. */
244
+ manifest: Theme;
205
245
  }
206
246
 
207
- export interface ManifestMeta {
247
+ export interface ThemeMeta {
208
248
  name: string;
209
249
  fileName: string;
210
250
  updatedAt: string;
211
251
  isActive: boolean;
252
+ /** The theme `tokens.generated.css` was last baked from. */
253
+ isProduction: boolean;
212
254
  /** `true` only for `default` — the protected baseline. Cannot be written
213
- * to or deleted, and theme/component Adopts cannot patch into it. */
255
+ * to or deleted, and colors-and-type / component Adopts cannot patch into
256
+ * it. */
214
257
  isProtected: boolean;
215
258
  }
@@ -99,6 +99,7 @@
99
99
  { path: 'getting-started', title: 'Getting started' },
100
100
  { path: 'editing-tokens', title: 'Editing tokens' },
101
101
  { path: 'themes-workflow', title: 'Themes' },
102
+ { path: 'where-themes-live', title: 'Where themes live' },
102
103
  { path: 'creating-components', title: 'Creating components' },
103
104
  ],
104
105
  },
@@ -14,6 +14,7 @@ export const chapters: Chapter[] = [
14
14
  { id: 'getting-started', title: 'Getting started' },
15
15
  { id: 'editing-tokens', title: 'Editing tokens' },
16
16
  { id: 'themes-workflow', title: 'Themes' },
17
+ { id: 'where-themes-live', title: 'Where themes live' },
17
18
  { id: 'creating-components', title: 'Creating components' },
18
19
  ];
19
20
 
@@ -1,7 +1,7 @@
1
1
  # Overview
2
2
 
3
3
  Live Tokens is a design system for building Svelte microsites quickly. You
4
- style your site by editing tokens and components in a live editor. When it looks right, you save the manifest and ship it.
4
+ style your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.
5
5
 
6
6
  ## How it works
7
7
 
@@ -65,10 +65,10 @@ live.
65
65
  ## Saving
66
66
 
67
67
  The editor saves to your browser continuously, so work survives a reload
68
- mid-edit. **Save** is a separate step: it writes a named theme file under
69
- `src/live-tokens/data/themes/`.
68
+ mid-edit. Writing a file is a separate step: the **Theme** panel at the foot of
69
+ the sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON
70
+ file under `src/live-tokens/data/themes/`.
70
71
 
71
- The header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`) and a file
72
- menu for New, Save, Save as, Switch, and Delete. You can keep many themes side
73
- by side; one is active at a time. See [Themes](themes-workflow.md) for the full
74
- lifecycle.
72
+ The header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep
73
+ many themes side by side; one is open at a time, and only **Adopt** publishes
74
+ one. See [Themes](themes-workflow.md) for the full lifecycle.
@@ -39,10 +39,10 @@ version upgrades never touch your styles. The package code stays in
39
39
  the page.
40
40
  3. Open **Palettes**, pick **Brand**, and change the base hex. The page
41
41
  repaints as you type.
42
- 4. Open the file menu and choose **Save as**. A theme appears as JSON under
43
- `src/live-tokens/data/themes/`.
44
- 5. Reload. Your saved theme is the active theme, so the page returns as you
45
- left it.
42
+ 4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your
43
+ theme appears as JSON under `src/live-tokens/data/themes/`.
44
+ 5. Reload. The editor reopens on your theme, so the page returns as you left
45
+ it.
46
46
 
47
47
  ## What you just changed
48
48
 
@@ -51,9 +51,11 @@ properties through `var(--...)`. There is no token build step and no
51
51
  preprocessor rewriting your code: the page renders against plain CSS variables
52
52
  the editor swaps live.
53
53
 
54
- To ship, promote a theme to production in the editor. That bakes the theme's
55
- variables into `src/live-tokens/data/tokens.generated.css`, which your build
56
- bundles alongside `tokens.css`. The editor itself never reaches production.
54
+ To ship, click **Adopt** in the Theme panel. That saves the open theme and bakes
55
+ it into `src/live-tokens/data/tokens.generated.css`, which your build bundles
56
+ alongside `tokens.css`. Adopt is the only action that changes what your site
57
+ ships, so try any look you like first. The editor itself never reaches
58
+ production.
57
59
 
58
60
  Already have a Svelte 5 + Vite app? The
59
61
  [README](https://github.com/motionproto/live-tokens#readme) covers installing