@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.
- package/.claude/skills/live-tokens-adjust-shape-space/SKILL.md +68 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
- package/.claude/skills/live-tokens-generate-theme/SKILL.md +152 -0
- package/CHANGELOG.md +292 -0
- package/README.md +37 -26
- package/bin/adjust.mjs +254 -0
- package/bin/cli.mjs +94 -7
- package/bin/generate-theme.mjs +251 -0
- package/bin/migrate.mjs +95 -6
- package/dist-plugin/adjust/index.cjs +259 -0
- package/dist-plugin/adjust/index.d.cts +45 -0
- package/dist-plugin/adjust/index.d.ts +45 -0
- package/dist-plugin/adjust/index.js +176 -0
- package/dist-plugin/{chunk-H4TRUINI.js → chunk-44RSTAII.js} +0 -48
- package/dist-plugin/chunk-6OZFXIQI.js +316 -0
- package/dist-plugin/chunk-76TFDTJO.js +556 -0
- package/dist-plugin/chunk-D3ZVKOR4.js +52 -0
- package/dist-plugin/dataPaths-DBN0RPuT.d.cts +54 -0
- package/dist-plugin/dataPaths-DBN0RPuT.d.ts +54 -0
- package/dist-plugin/generateColorsAndType/index.cjs +1781 -0
- package/dist-plugin/generateColorsAndType/index.d.cts +81 -0
- package/dist-plugin/generateColorsAndType/index.d.ts +81 -0
- package/dist-plugin/generateColorsAndType/index.js +1210 -0
- package/dist-plugin/index.cjs +901 -533
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +658 -1082
- package/dist-plugin/migrateData/index.cjs +728 -0
- package/dist-plugin/migrateData/index.d.cts +60 -0
- package/dist-plugin/migrateData/index.d.ts +60 -0
- package/dist-plugin/migrateData/index.js +348 -0
- package/dist-plugin/themeTypes-DMHZOnUn.d.cts +211 -0
- package/dist-plugin/themeTypes-DMHZOnUn.d.ts +211 -0
- package/dist-plugin/tokensCssMigrations/index.cjs +2 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +3 -15
- package/dist-plugin/tokensCssMigrations/index.d.ts +3 -15
- package/dist-plugin/tokensCssMigrations/index.js +4 -2
- package/package.json +18 -4
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +183 -158
- package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +0 -4
- package/src/editor/component-editor/scaffolding/SaveAsDialog.svelte +4 -4
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +2 -59
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +1 -1
- package/src/editor/core/components/adjustAliases.ts +180 -0
- package/src/editor/core/components/aliasKinds.ts +73 -0
- package/src/editor/core/components/componentConfigService.ts +26 -38
- package/src/editor/core/flashStatus.ts +1 -1
- package/src/editor/core/fonts/fontMigration.ts +13 -13
- package/src/editor/core/fonts/fontPairing.ts +35 -0
- package/src/editor/core/palettes/paletteDerivation.ts +128 -16
- package/src/editor/core/preview/lookPreview.ts +141 -0
- package/src/editor/core/productionPulse.ts +34 -21
- package/src/editor/core/storage/files/versionedFileResourceClient.ts +18 -45
- package/src/editor/core/store/editorConfigStore.ts +2 -2
- package/src/editor/core/store/editorPersistence.ts +0 -1
- package/src/editor/core/store/editorStore.ts +114 -51
- package/src/editor/core/store/editorTypes.ts +0 -1
- package/src/editor/core/store/gradientSource.ts +2 -2
- package/src/editor/core/themes/colorsAndTypeService.ts +95 -0
- package/src/editor/core/themes/generateColorsAndType.ts +433 -0
- package/src/editor/core/themes/loadRows.ts +64 -0
- package/src/editor/core/themes/lookSummary.ts +75 -0
- package/src/editor/core/themes/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +3 -3
- package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -2
- package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +3 -3
- package/src/editor/core/themes/migrations/2026-08-13-drop-legacy-shape-space-keys.ts +38 -0
- package/src/editor/core/themes/migrations/2026-08-15-line-height-scale-rename.ts +57 -0
- package/src/editor/core/themes/migrations/index.ts +19 -11
- package/src/editor/core/themes/slices/components.ts +4 -4
- package/src/editor/core/themes/slices/fonts.ts +1 -1
- package/src/editor/core/themes/slices/gradients.ts +22 -0
- package/src/editor/core/themes/slices/palettes.ts +2 -2
- package/src/editor/core/themes/themeInit.ts +21 -29
- package/src/editor/core/themes/themeService.ts +218 -78
- package/src/editor/core/themes/themeTypes.ts +94 -51
- package/src/editor/docs/Docs.svelte +1 -0
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/01-overview.md +1 -1
- package/src/editor/docs/content/editing-tokens.md +6 -6
- package/src/editor/docs/content/getting-started.md +9 -7
- package/src/editor/docs/content/themes-workflow.md +74 -34
- package/src/editor/docs/content/where-themes-live.md +61 -0
- package/src/editor/docs/content.generated.ts +5 -4
- package/src/editor/index.ts +27 -27
- package/src/editor/pages/ComponentEditorPage.svelte +2 -2
- package/src/editor/pages/EditorShell.svelte +4 -30
- package/src/editor/ui/BezierCurveEditor.svelte +2 -2
- package/src/editor/ui/ColorEditPanel.svelte +1 -1
- package/src/editor/ui/FileLoadList.svelte +67 -7
- package/src/editor/ui/GradientEditor.svelte +3 -3
- package/src/editor/ui/PaletteEditor.svelte +26 -12
- package/src/editor/ui/ThemePanel.svelte +1067 -0
- package/src/editor/ui/UIDialog.svelte +11 -9
- package/src/editor/ui/UIPillButton.svelte +7 -2
- package/src/editor/ui/colors/ColorWheel.svelte +34 -1
- package/src/editor/ui/curveEngine.ts +48 -0
- package/src/editor/ui/index.ts +4 -2
- package/src/editor/ui/palette/PaletteBase.svelte +75 -51
- package/src/editor/ui/palette/PaletteJumpButton.svelte +3 -8
- package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
- package/src/live-tokens/data/{themes → colors-and-type}/default.json +816 -293
- package/src/live-tokens/data/colors-and-type/halloween.json +2529 -0
- package/src/live-tokens/data/colors-and-type/midnight-study.json +2536 -0
- package/src/live-tokens/data/colors-and-type/ocean.json +2489 -0
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +2520 -0
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +2476 -0
- package/src/live-tokens/data/colors-and-type/sunset.json +2528 -0
- package/src/live-tokens/data/themes/autumn.json +3918 -0
- package/src/live-tokens/data/themes/halloween.json +3992 -0
- package/src/live-tokens/data/themes/midnight-study.json +3932 -0
- package/src/live-tokens/data/themes/ocean.json +3917 -0
- package/src/live-tokens/data/themes/royal-velvet.json +3983 -0
- package/src/live-tokens/data/themes/spring-meadow.json +3904 -0
- package/src/live-tokens/data/themes/sunset.json +3931 -0
- package/src/live-tokens/data/tokens.generated.css +106 -117
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/fonts.css +1 -1
- package/template/README.md +7 -5
- package/template/_gitignore +0 -6
- package/src/editor/core/components/componentPersist.ts +0 -62
- package/src/editor/core/manifests/manifestService.ts +0 -172
- package/src/editor/ui/ManifestFileManager.svelte +0 -446
- package/src/editor/ui/ThemeFileManager.svelte +0 -785
- 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 {
|
|
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
|
-
|
|
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 {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
65
|
-
let
|
|
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
|
-
//
|
|
79
|
-
let
|
|
80
|
-
let
|
|
81
|
-
let
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
197
|
+
function currentConfigData(displayName: string): ComponentConfig {
|
|
142
198
|
const now = new Date().toISOString();
|
|
143
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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(
|
|
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
|
|
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 +
|
|
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
|
|
202
|
-
loadComponentActive(component,
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 =
|
|
295
|
+
const adoptingName = openTheme.name;
|
|
249
296
|
productionUpdateStatus = 'updating';
|
|
250
297
|
try {
|
|
251
|
-
if (wasDirty)
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
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
|
|
282
|
-
|
|
313
|
+
async function onThemeSaveAs(detail: { displayName: string; fileName: string }) {
|
|
314
|
+
themeSaveAsDialog = false;
|
|
283
315
|
try {
|
|
284
|
-
|
|
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
|
|
287
|
-
|
|
322
|
+
window.alert(`Failed to create the theme: ${(err as Error).message}`);
|
|
323
|
+
retryAdoptAfterThemeSave = false;
|
|
288
324
|
return;
|
|
289
325
|
}
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
await
|
|
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
|
|
300
|
-
loadComponentActive(component,
|
|
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 || (
|
|
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={
|
|
333
|
-
isProtected={
|
|
368
|
+
name={editorName}
|
|
369
|
+
isProtected={editorDoc === 'default'}
|
|
334
370
|
dirty={compDirty}
|
|
335
371
|
applied={isApplied}
|
|
336
|
-
protectedTitle="
|
|
372
|
+
protectedTitle="Running the config shipped with the component"
|
|
337
373
|
title={compDirty
|
|
338
374
|
? 'Unsaved changes'
|
|
339
375
|
: isApplied
|
|
340
|
-
? '
|
|
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 {
|
|
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={
|
|
381
|
-
isProtected={
|
|
382
|
-
protectedTitle="
|
|
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={
|
|
396
|
-
disabled={productionUpdateStatus === 'updating' ||
|
|
397
|
-
title={
|
|
398
|
-
? ''
|
|
399
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
459
|
+
currentDisplayName={editorName}
|
|
433
460
|
{files}
|
|
434
|
-
currentFileName={activeFileName}
|
|
435
461
|
reservedDisplayNames={files.filter((f) => f.fileName === 'default').map((f) => f.name)}
|
|
436
|
-
title=
|
|
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={
|
|
445
|
-
currentDisplayName="
|
|
446
|
-
files={
|
|
447
|
-
reservedDisplayNames={
|
|
448
|
-
title="Save
|
|
449
|
-
placeholder="
|
|
450
|
-
description="Adopting
|
|
451
|
-
reservedNameMessage='That name is reserved for the protected default
|
|
452
|
-
onsave={
|
|
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/
|
|
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,
|
|
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
|
|
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;
|
|
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
|