@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
@@ -6,15 +6,19 @@
6
6
  import { onMount, onDestroy } from 'svelte';
7
7
  import UIInfoPopover from '../../ui/UIInfoPopover.svelte';
8
8
  import { get } from 'svelte/store';
9
- import type { AliasDiskValue, ComponentConfig, ComponentConfigMeta } from '../../core/themes/themeTypes';
9
+ import type {
10
+ AliasDiskValue,
11
+ ComponentConfig,
12
+ ComponentConfigMeta,
13
+ LiveSource,
14
+ } from '../../core/themes/themeTypes';
10
15
  import { componentSourceFile } from './componentSources';
11
16
  import {
17
+ getActiveComponentConfig,
12
18
  loadComponentConfig,
13
19
  saveComponentConfig,
14
20
  deleteComponentConfig,
15
- setActiveComponentFile,
16
- setComponentProductionFile,
17
- type ComponentProductionInfo,
21
+ writeWorkingComponentConfig,
18
22
  } from '../../core/components/componentConfigService';
19
23
  import {
20
24
  editorState,
@@ -22,9 +26,22 @@
22
26
  loadComponentActive,
23
27
  markComponentSaved,
24
28
  } from '../../core/store/editorStore';
25
- import { bumpProductionRevision } from '../../core/productionPulse';
26
- import { listManifests, saveAsManifest } from '../../core/manifests/manifestService';
27
- import type { ManifestMeta } from '../../core/themes/themeTypes';
29
+ import {
30
+ bumpComponentActiveRevision,
31
+ bumpProductionRevision,
32
+ liveMovedSinceBake,
33
+ productionRevision,
34
+ productionTheme,
35
+ } from '../../core/productionPulse';
36
+ import {
37
+ adoptLook,
38
+ getActiveTheme,
39
+ getProductionTheme,
40
+ listThemes,
41
+ saveActiveTheme,
42
+ saveAsTheme,
43
+ } from '../../core/themes/themeService';
44
+ import type { ThemeMeta } from '../../core/themes/themeTypes';
28
45
  import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../core/themes/migrations';
29
46
  import { refToDiskValue } from '../../core/store/cssVarRef';
30
47
  import { safeFetch } from '../../core/storage/storage';
@@ -36,17 +53,14 @@
36
53
  import UIPillButton from '../../ui/UIPillButton.svelte';
37
54
  import UISquareButton from '../../ui/UISquareButton.svelte';
38
55
 
39
-
40
-
41
-
42
56
  interface Props {
43
57
  /** Which component this manager controls (e.g. "button"). */
44
58
  component: string;
45
59
  /** Display name shown at the start of the bar (e.g. "Segmented Control"). */
46
60
  title?: string;
47
- /** When provided, renders a Reset button that reverts the component to its
48
- currently-loaded config file (discarding unsaved edits). To switch
49
- configs or return to default, use the File menu. */
61
+ /** When provided, renders a Reset button that reverts the component to the
62
+ config the server holds for it (discarding unsaved edits). To load
63
+ another config, use the File menu. */
50
64
  resetVariables?: string[] | null;
51
65
  }
52
66
 
@@ -60,57 +74,85 @@
60
74
  let saveStatus: SaveStatus = 'idle';
61
75
  const setSaveStatus = (s: SaveStatus) => (saveStatus = s);
62
76
 
77
+ // Saved presets. Nothing live points at one: the component runs its working
78
+ // buffer, and the theme it belongs to carries the copy that ships.
63
79
  let files: ComponentConfigMeta[] = $state([]);
64
- let activeFileName = $state('default');
65
- let currentDisplayName = $state('Default');
80
+ let liveSource: LiveSource = $state('default');
81
+ let openTheme: { fileName: string; name: string } = $state({
82
+ fileName: 'default',
83
+ name: 'Motion Proto',
84
+ });
66
85
  let saveAsDialog = $state(false);
67
- // Title + description re-set per trigger so the auto-prompts (Save on
68
- // default, Adopt on default+dirty) explain why they appeared, while the
69
- // manual Save As button stays terse.
70
- let saveAsTitle = $state('Save Component As');
71
- let saveAsDescription = $state('');
72
86
 
73
- let productionInfo = $state<ComponentProductionInfo | null>(null);
74
87
  type ProductionStatus = 'idle' | 'updating' | 'done' | 'error';
75
88
  let productionUpdateStatus: ProductionStatus = $state('idle');
76
89
  let adoptFeedback = $state('');
77
90
 
78
- // Manifest SaveAs prompt for the "Adopt while default manifest is active" case.
79
- let manifestSaveAsDialog = $state(false);
80
- let manifests: ManifestMeta[] = $state([]);
81
- let retryAdoptAfterManifestSave = false;
91
+ // Theme SaveAs prompt for the "Adopt while the default theme is open" case.
92
+ let themeSaveAsDialog = $state(false);
93
+ let themes: ThemeMeta[] = $state([]);
94
+ let retryAdoptAfterThemeSave = false;
82
95
 
83
96
  const setProductionStatus = (s: ProductionStatus) => (productionUpdateStatus = s);
84
97
 
85
98
  let compDirty = $derived($componentDirty[component] ?? false);
86
- let isApplied = $derived(!!productionInfo && productionInfo.fileName === activeFileName && !compDirty);
99
+ let openIsProtected = $derived(openTheme.fileName === 'default');
100
+ // Both rows name a document, because that is what a config belongs to now:
101
+ // the component runs the open theme's config (or the shipped default), and
102
+ // production ships the published theme's.
103
+ let editorDoc = $derived(liveSource === 'default' ? 'default' : openTheme.fileName);
104
+ let productionDoc = $derived(
105
+ $productionTheme
106
+ ? ($productionTheme.componentConfigs[component] ? $productionTheme._fileName ?? 'default' : 'default')
107
+ : null,
108
+ );
109
+ let editorName = $derived(liveSource === 'default' ? 'Default' : openTheme.name);
110
+ let productionName = $derived(
111
+ productionDoc === null ? '—' : productionDoc === 'default' ? 'Default' : $productionTheme?.name ?? '—',
112
+ );
113
+ // Saving writes the buffer, which the bake has not seen: the document is
114
+ // still the published one, so only the shared signal can say the config on
115
+ // screen is not what production runs.
116
+ let isApplied = $derived(
117
+ productionDoc !== null && productionDoc === editorDoc && !compDirty && !$liveMovedSinceBake,
118
+ );
87
119
  let resetDirty = $derived(!!resetVariables && compDirty);
88
120
 
89
121
  async function refreshFiles() {
90
122
  // safeFetch returns null on dev-server unavailable / non-2xx — silently
91
123
  // leave the file list empty in that case.
92
- const data = await safeFetch<{
93
- files: ComponentConfigMeta[];
94
- activeFile: string;
95
- }>(`${API_BASE}/component-configs/${encodeURIComponent(component)}`);
96
- if (!data) return;
97
- files = data.files;
98
- activeFileName = data.activeFile;
99
- const active = files.find((f) => f.fileName === activeFileName);
100
- if (active) currentDisplayName = active.name;
124
+ const data = await safeFetch<{ files: ComponentConfigMeta[] }>(
125
+ `${API_BASE}/component-configs/${encodeURIComponent(component)}`,
126
+ );
127
+ if (data) files = data.files;
128
+ }
129
+
130
+ /** Which layer the component's live config comes from, and the theme it
131
+ * belongs to. Both ride on the live read door's markers. */
132
+ async function refreshLive() {
133
+ const cfg = await getActiveComponentConfig(component);
134
+ if (cfg) liveSource = cfg._source ?? 'default';
135
+ try {
136
+ const theme = await getActiveTheme();
137
+ if (theme) openTheme = { fileName: theme._fileName ?? 'default', name: theme.name };
138
+ } catch {
139
+ // silent — keep the last answer
140
+ }
101
141
  }
102
142
 
103
143
  async function refreshProduction() {
104
- // Preserve existing productionInfo on transient fetch failure rather than
105
- // clobbering it to null — same behaviour as the previous empty catch.
106
- const info = await safeFetch<ComponentProductionInfo>(
107
- `${API_BASE}/component-configs/${encodeURIComponent(component)}/production`,
108
- );
109
- if (info) productionInfo = info;
144
+ // Preserve the last answer on a transient failure rather than clobbering
145
+ // the store to null, which would read as "production unknown".
146
+ try {
147
+ productionTheme.set(await getProductionTheme());
148
+ } catch {
149
+ // silent
150
+ }
110
151
  }
111
152
 
112
153
  onMount(async () => {
113
154
  await refreshFiles();
155
+ await refreshLive();
114
156
  await refreshProduction();
115
157
  window.addEventListener('keydown', handleKeydown);
116
158
  });
@@ -119,6 +161,20 @@
119
161
  window.removeEventListener('keydown', handleKeydown);
120
162
  });
121
163
 
164
+ // An Adopt in the theme panel can fork the protected theme, so the document
165
+ // this component belongs to changes under us. The production side rides on
166
+ // the shared store and needs no re-read here. Skip the first tick — mount
167
+ // ran it already.
168
+ let pulseInitialised = false;
169
+ $effect(() => {
170
+ void $productionRevision;
171
+ if (!pulseInitialised) {
172
+ pulseInitialised = true;
173
+ return;
174
+ }
175
+ refreshLive();
176
+ });
177
+
122
178
  function handleKeydown(e: KeyboardEvent) {
123
179
  if ((e.metaKey || e.ctrlKey) && e.key === 's') {
124
180
  e.preventDefault();
@@ -138,9 +194,9 @@
138
194
  return get(editorState).components[component]?.config ?? {};
139
195
  }
140
196
 
141
- async function persist(fileName: string, displayName: string): Promise<void> {
197
+ function currentConfigData(displayName: string): ComponentConfig {
142
198
  const now = new Date().toISOString();
143
- const data: ComponentConfig = {
199
+ return {
144
200
  name: displayName,
145
201
  component,
146
202
  createdAt: now,
@@ -149,41 +205,40 @@
149
205
  config: { ...currentConfig() },
150
206
  schemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
151
207
  };
152
- await saveComponentConfig(component, fileName, data);
153
- await setActiveComponentFile(component, fileName);
154
- activeFileName = fileName;
155
- currentDisplayName = displayName;
208
+ }
209
+
210
+ /** Flush the editor state to the component's working buffer. A theme save
211
+ * captures the buffers, so this is what puts these edits in reach of Save
212
+ * and Adopt. */
213
+ async function persist(): Promise<void> {
214
+ await writeWorkingComponentConfig(component, currentConfigData(openTheme.name));
215
+ liveSource = 'working';
156
216
  markComponentSaved(component);
217
+ bumpComponentActiveRevision();
157
218
  }
158
219
 
159
220
  async function handleSave() {
160
- if (activeFileName === 'default') {
161
- // Default is regenerated from source, so the first Save quietly opens
162
- // Save As — same dialog as the Save As button, no scolding description.
163
- openSaveAs();
164
- return;
165
- }
166
221
  saveStatus = 'saving';
167
222
  try {
168
- await persist(activeFileName, currentDisplayName);
223
+ await persist();
169
224
  flashStatus(setSaveStatus, 'saved');
170
- await refreshFiles();
171
225
  } catch {
172
226
  flashStatus(setSaveStatus, 'error');
173
227
  }
174
228
  }
175
229
 
176
230
  function openSaveAs() {
177
- saveAsTitle = 'Save Component As';
178
- saveAsDescription = '';
179
231
  saveAsDialog = true;
180
232
  }
181
233
 
234
+ /** Save As writes a preset — a config the user can load into any theme —
235
+ * and keeps the buffer, so the editor reads saved either way. */
182
236
  async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
183
237
  const { displayName, fileName } = detail;
184
238
  saveStatus = 'saving';
185
239
  try {
186
- await persist(fileName, displayName);
240
+ await saveComponentConfig(component, fileName, currentConfigData(displayName));
241
+ await persist();
187
242
  flashStatus(setSaveStatus, 'saved');
188
243
  await refreshFiles();
189
244
  } catch {
@@ -192,16 +247,16 @@
192
247
  }
193
248
 
194
249
  async function handleLoad(file: ComponentConfigMeta) {
195
- // Multi-step service flow (load + set-active) — if any network call
250
+ // Multi-step service flow (load + buffer write) — if any network call
196
251
  // fails, the dialog is already closed and the local state stays on the
197
252
  // previous selection. Silent by design; the same boot resilience that
198
253
  // keeps the editor working without a dev-server applies here.
199
254
  try {
200
- const cfg = await loadComponentConfig(component, file.fileName);
201
- await setActiveComponentFile(component, file.fileName);
202
- loadComponentActive(component, file.fileName, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
203
- activeFileName = file.fileName;
204
- currentDisplayName = file.name;
255
+ const { _fileName, _source, ...cfg } = await loadComponentConfig(component, file.fileName);
256
+ await writeWorkingComponentConfig(component, { ...cfg, component });
257
+ loadComponentActive(component, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
258
+ liveSource = 'working';
259
+ bumpComponentActiveRevision();
205
260
  } catch {
206
261
  // intentional: see comment above
207
262
  }
@@ -209,95 +264,76 @@
209
264
 
210
265
  async function handleDelete(file: ComponentConfigMeta) {
211
266
  if (file.fileName === 'default') return;
212
- // Multi-step service flow (delete + reload-default-on-active-removal).
213
- // Silent by design — see handleLoad.
267
+ // A preset is not live state, so deleting one leaves the component running
268
+ // exactly what it ran. Silent by design — see handleLoad.
214
269
  try {
215
- // Capture before refreshFiles() reads the server's reverted active back
216
- // into local state — otherwise the "was this the active file?" check
217
- // below sees the post-revert value and skips the reload.
218
- const wasActive = file.fileName === activeFileName;
219
270
  await deleteComponentConfig(component, file.fileName);
220
271
  await refreshFiles();
221
- await refreshProduction();
222
- if (wasActive) {
223
- // Server reverts active to default; reload default aliases into the store
224
- // so the deleted file's CSS vars are replaced by default's.
225
- const defaultCfg = await loadComponentConfig(component, 'default');
226
- loadComponentActive(component, 'default', defaultCfg.aliases, defaultCfg.config, defaultCfg.schemaVersion ?? 0);
227
- activeFileName = 'default';
228
- currentDisplayName = 'Default';
229
- }
230
272
  } catch {
231
273
  // intentional: see comment above
232
274
  }
233
275
  }
234
276
 
235
- async function handleUpdateProduction() {
236
- // If the editor has unsaved edits, persist them first so production adopts
237
- // the actual current state — not a stale snapshot. The `default` file is
238
- // regenerated from source and can't be overwritten, so route to Save As
239
- // and bail; the user can re-trigger Adopt after the new file is saved.
240
- if (compDirty && activeFileName === 'default') {
241
- saveAsTitle = 'Save Component As';
242
- saveAsDescription =
243
- 'Adopting pushes this component to production. The default config is read-only, so save your edits to a new file first.';
244
- saveAsDialog = true;
277
+ /**
278
+ * Adopt ships the whole look, because production is one saved theme: flush
279
+ * this component, save the open theme, publish it. The protected Default
280
+ * theme cannot record what shipped, so it prompts for a theme name first and
281
+ * retries.
282
+ */
283
+ async function handleAdopt() {
284
+ if (openIsProtected) {
285
+ retryAdoptAfterThemeSave = true;
286
+ try {
287
+ themes = await listThemes();
288
+ } catch {
289
+ themes = [];
290
+ }
291
+ themeSaveAsDialog = true;
245
292
  return;
246
293
  }
247
294
  const wasDirty = compDirty;
248
- const adoptingName = currentDisplayName;
295
+ const adoptingName = openTheme.name;
249
296
  productionUpdateStatus = 'updating';
250
297
  try {
251
- if (wasDirty) {
252
- await persist(activeFileName, currentDisplayName);
253
- await refreshFiles();
254
- }
255
- await setComponentProductionFile(component, activeFileName);
298
+ if (wasDirty) await persist();
299
+ await saveActiveTheme();
300
+ await adoptLook();
256
301
  await refreshProduction();
257
302
  bumpProductionRevision();
258
303
  adoptFeedback = wasDirty
259
304
  ? `Saved "${adoptingName}" and adopted`
260
305
  : `Adopted "${adoptingName}"`;
261
306
  flashStatus(setProductionStatus, 'done', { onIdle: () => (adoptFeedback = '') });
262
- } catch (err) {
263
- const e = err as Error & { code?: string };
264
- if (e.code === 'ACTIVE_IS_PROTECTED') {
265
- adoptFeedback = '';
266
- productionUpdateStatus = 'idle';
267
- retryAdoptAfterManifestSave = true;
268
- try {
269
- manifests = await listManifests();
270
- } catch {
271
- manifests = [];
272
- }
273
- manifestSaveAsDialog = true;
274
- return;
275
- }
307
+ } catch {
276
308
  adoptFeedback = '';
277
309
  flashStatus(setProductionStatus, 'error', { onIdle: () => (adoptFeedback = '') });
278
310
  }
279
311
  }
280
312
 
281
- async function onManifestSaveAs(detail: { displayName: string; fileName: string }) {
282
- manifestSaveAsDialog = false;
313
+ async function onThemeSaveAs(detail: { displayName: string; fileName: string }) {
314
+ themeSaveAsDialog = false;
283
315
  try {
284
- await saveAsManifest(detail.fileName, detail.displayName);
316
+ // The capture reads the server's live state, so this component's edits
317
+ // have to be in its buffer before the new theme takes shape.
318
+ if (compDirty) await persist();
319
+ await saveAsTheme(detail.fileName, detail.displayName);
320
+ await refreshLive();
285
321
  } catch (err) {
286
- window.alert(`Failed to create manifest: ${(err as Error).message}`);
287
- retryAdoptAfterManifestSave = false;
322
+ window.alert(`Failed to create the theme: ${(err as Error).message}`);
323
+ retryAdoptAfterThemeSave = false;
288
324
  return;
289
325
  }
290
- if (retryAdoptAfterManifestSave) {
291
- retryAdoptAfterManifestSave = false;
292
- await handleUpdateProduction();
326
+ if (retryAdoptAfterThemeSave) {
327
+ retryAdoptAfterThemeSave = false;
328
+ await handleAdopt();
293
329
  }
294
330
  }
295
331
 
296
332
  async function handleReset() {
297
333
  if (!resetVariables) return;
298
334
  try {
299
- const cfg = await loadComponentConfig(component, activeFileName);
300
- loadComponentActive(component, activeFileName, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
335
+ const cfg = await getActiveComponentConfig(component);
336
+ if (cfg) loadComponentActive(component, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
301
337
  } catch {
302
338
  // intentional: dev-server unavailable — leave state untouched
303
339
  }
@@ -318,7 +354,7 @@
318
354
  {/if}
319
355
  </div>
320
356
 
321
- <div class="cfm-rows" class:in-sync={isApplied} class:promotable={compDirty || (!!productionInfo && productionInfo.fileName !== activeFileName)}>
357
+ <div class="cfm-rows" class:in-sync={isApplied} class:promotable={compDirty || (productionDoc !== null && productionDoc !== editorDoc)}>
322
358
  <div class="cfm-row cfm-row-editor" class:dirty={compDirty} class:applied={isApplied}>
323
359
  <span class="cfm-rail" aria-hidden="true"></span>
324
360
  <div class="cfm-row-head">
@@ -329,15 +365,15 @@
329
365
  </span>
330
366
  </div>
331
367
  <FilePill
332
- name={currentDisplayName}
333
- isProtected={activeFileName === 'default'}
368
+ name={editorName}
369
+ isProtected={editorDoc === 'default'}
334
370
  dirty={compDirty}
335
371
  applied={isApplied}
336
- protectedTitle="Protected system config"
372
+ protectedTitle="Running the config shipped with the component"
337
373
  title={compDirty
338
374
  ? 'Unsaved changes'
339
375
  : isApplied
340
- ? 'Active config is applied to production'
376
+ ? 'Production is running this config'
341
377
  : ''}
342
378
  style="flex: 0 1 11.25rem; min-width: 0; max-width: 11.25rem;"
343
379
  />
@@ -345,7 +381,6 @@
345
381
  <ComponentFileMenu
346
382
  {component}
347
383
  {files}
348
- {activeFileName}
349
384
  onsave={handleSave}
350
385
  onsaveAs={openSaveAs}
351
386
  onopenLoad={refreshFiles}
@@ -357,7 +392,7 @@
357
392
  icon="fa-rotate-left"
358
393
  onclick={handleReset}
359
394
  disabled={!resetDirty}
360
- title="Revert unsaved changes to {currentDisplayName}"
395
+ title="Revert unsaved changes to {editorName}"
361
396
  >Reset</UISquareButton>
362
397
  {/if}
363
398
  </div>
@@ -377,9 +412,9 @@
377
412
  </span>
378
413
  </div>
379
414
  <FilePill
380
- name={productionInfo?.name ?? '—'}
381
- isProtected={productionInfo?.fileName === 'default'}
382
- protectedTitle="Protected system config"
415
+ name={productionName}
416
+ isProtected={productionDoc === 'default'}
417
+ protectedTitle="Running the config shipped with the component"
383
418
  style="flex: 0 1 11.25rem; min-width: 0; max-width: 11.25rem;"
384
419
  />
385
420
  <div class="cfm-actions">
@@ -392,31 +427,23 @@
392
427
  : productionUpdateStatus === 'error'
393
428
  ? 'fa-xmark'
394
429
  : 'fa-arrow-down'}
395
- onclick={handleUpdateProduction}
396
- disabled={productionUpdateStatus === 'updating' || !productionInfo || (productionInfo.fileName === activeFileName && !compDirty)}
397
- title={!productionInfo
398
- ? ''
399
- : productionInfo.fileName === activeFileName && !compDirty
400
- ? 'Already in sync with editor'
401
- : compDirty && activeFileName === 'default'
402
- ? 'Save edits as a new file, then adopt'
403
- : compDirty
404
- ? `Save "${currentDisplayName}" and adopt`
405
- : `Adopt "${currentDisplayName}" from editor`}
430
+ onclick={handleAdopt}
431
+ disabled={productionUpdateStatus === 'updating' || isApplied}
432
+ title={isApplied
433
+ ? 'Production is running this theme'
434
+ : `Save "${openTheme.name}" and ship it to production`}
406
435
  >
407
436
  {#if productionUpdateStatus === 'idle'}Adopt{:else if productionUpdateStatus === 'updating'}Adopting{:else if productionUpdateStatus === 'done'}Adopted{:else}Error{/if}
408
437
  </UISquareButton>
409
438
  <UIInfoPopover title="Component Configuration" ariaLabel="About Save and Adopt">
410
439
  <p>
411
- Editor and Prod both use a saved file. When they share the
412
- <em>same</em> file, <strong>Saved changes</strong> go to into production
413
- immediately. They are sharing the configuration.
440
+ <strong>Save</strong> keeps your edits in the theme you have open. The theme carries a copy of every component you changed, so the look travels as one file.
414
441
  </p>
415
442
  <p>
416
- To experiment without changing production,<strong>Save As</strong> a new file first.
443
+ <strong>Save As</strong> writes the component's settings to a file of their own, ready to load into any theme.
417
444
  </p>
418
445
  <p>
419
- When ready, click <strong>Adopt</strong> to use the new file on prod.
446
+ <strong>Adopt</strong> ships the whole theme to production, this component with it. Production runs one saved theme, so a component never ships alone.
420
447
  </p>
421
448
  </UIInfoPopover>
422
449
  {#if adoptFeedback}
@@ -429,27 +456,25 @@
429
456
 
430
457
  <SaveAsDialog
431
458
  bind:show={saveAsDialog}
432
- {currentDisplayName}
459
+ currentDisplayName={editorName}
433
460
  {files}
434
- currentFileName={activeFileName}
435
461
  reservedDisplayNames={files.filter((f) => f.fileName === 'default').map((f) => f.name)}
436
- title={saveAsTitle}
437
- description={saveAsDescription}
462
+ title="Save Component As"
438
463
  placeholder="Component config name…"
439
464
  branchFromDefaultName={`my-${(title || 'component').toLowerCase().trim().replace(/\s+/g, '-')}`}
440
465
  onsave={confirmSaveAs}
441
466
  />
442
467
 
443
468
  <SaveAsDialog
444
- bind:show={manifestSaveAsDialog}
445
- currentDisplayName="my-manifest"
446
- files={manifests}
447
- reservedDisplayNames={manifests.filter((m) => m.fileName === 'default').map((m) => m.name)}
448
- title="Save Manifest As"
449
- placeholder="Manifest name…"
450
- description="Adopting a component change updates the active manifest, The default manifest is locked. Name a new manifest for the site."
451
- reservedNameMessage='That name is reserved for the protected default manifest.'
452
- onsave={onManifestSaveAs}
469
+ bind:show={themeSaveAsDialog}
470
+ currentDisplayName="My Theme"
471
+ files={themes}
472
+ reservedDisplayNames={themes.filter((m) => m.fileName === 'default').map((m) => m.name)}
473
+ title="Save Theme As"
474
+ placeholder="Theme name…"
475
+ description="Adopting ships the whole theme, this component with it. The default theme is locked, so name a new theme for this site."
476
+ reservedNameMessage='That name is reserved for the protected default theme.'
477
+ onsave={onThemeSaveAs}
453
478
  />
454
479
 
455
480
  <style>
@@ -9,8 +9,6 @@
9
9
  component: string;
10
10
  /** Files shown in the load dialog. */
11
11
  files?: ComponentConfigMeta[];
12
- /** Currently active file — highlighted in the load list. */
13
- activeFileName?: string;
14
12
  onsave?: () => void;
15
13
  onsaveAs?: () => void;
16
14
  /** Fired when the user clicks "Load…" in the menu — parent should refresh `files`. */
@@ -22,7 +20,6 @@
22
20
  let {
23
21
  component,
24
22
  files = [],
25
- activeFileName = 'default',
26
23
  onsave,
27
24
  onsaveAs,
28
25
  onopenLoad,
@@ -107,7 +104,6 @@
107
104
  bind:show={showFileList}
108
105
  title="Load {component} Config"
109
106
  {files}
110
- {activeFileName}
111
107
  systemBadge={{ label: 'System', title: 'Protected system config' }}
112
108
  emptyMessage="No saved files"
113
109
  onload={handleLoad}
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { run } from 'svelte/legacy';
3
3
 
4
- import { sanitizeFileName } from '../../core/themes/themeService';
4
+ import { sanitizeFileName } from '../../core/themes/colorsAndTypeService';
5
5
  import UIDialog from '../../ui/UIDialog.svelte';
6
6
 
7
7
  interface Props {
@@ -11,15 +11,15 @@
11
11
  currentDisplayName?: string;
12
12
  /** Existing files used by the increment helper to find the next available `_NN` suffix.
13
13
  * Only `fileName` is read, so this accepts any shape with that field
14
- * (ComponentConfigMeta, ManifestMeta, …). */
14
+ * (ComponentConfigMeta, ThemeMeta, …). */
15
15
  files?: { fileName: string }[];
16
16
  /** Dialog title — defaults to "Save As". Overridable so callers can use
17
- * context-specific framing (e.g. "Save Manifest As"). */
17
+ * context-specific framing (e.g. "Save Theme As"). */
18
18
  title?: string;
19
19
  /** Placeholder shown in the empty input. */
20
20
  placeholder?: string;
21
21
  /** Error message shown when the user types the reserved "default" name.
22
- * Default copy references components; manifests should override. */
22
+ * Default copy references components; the theme panel overrides it. */
23
23
  reservedNameMessage?: string;
24
24
  /** Optional one-line explanation rendered above the name input. Use when
25
25
  * the dialog opens automatically (e.g. as a recovery prompt) so the user