@motion-proto/live-tokens 0.47.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +270 -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 +849 -531
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +658 -1132
- 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 +83 -13
- 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/FileLoadList.svelte +67 -7
- package/src/editor/ui/GradientEditor.svelte +3 -3
- package/src/editor/ui/ThemePanel.svelte +1067 -0
- package/src/editor/ui/UIDialog.svelte +11 -9
- package/src/editor/ui/index.ts +4 -2
- package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
- package/src/live-tokens/data/{themes → colors-and-type}/default.json +962 -549
- 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 +129 -140
- 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
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import type { Manifest, ManifestMeta, ManifestBundle, Theme, ComponentConfig } from '../themes/themeTypes';
|
|
2
|
-
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
|
-
import { API_BASE } from '../storage/apiBase';
|
|
4
|
-
import { listComponents } from '../components/componentConfigService';
|
|
5
|
-
import { getActiveTheme } from '../themes/themeService';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* REST client for manifest files. A manifest references one theme file +
|
|
9
|
-
* one config file per component (by basename). The active manifest is the
|
|
10
|
-
* single live snapshot: theme and component Adopts patch its refs server-side.
|
|
11
|
-
*
|
|
12
|
-
* `default` is the protected baseline — cannot be overwritten or deleted, and
|
|
13
|
-
* Adopts return 409 ACTIVE_IS_PROTECTED while it is active.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const manifestsResource = versionedFileResource<Manifest, ManifestMeta, never>({
|
|
17
|
-
baseUrl: `${API_BASE}/manifests`,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
export const listManifests = async (): Promise<ManifestMeta[]> => {
|
|
21
|
-
const data = await manifestsResource.list();
|
|
22
|
-
return data.files;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const loadManifest = (fileName: string): Promise<Manifest> =>
|
|
26
|
-
manifestsResource.load(fileName);
|
|
27
|
-
export const saveManifest = (fileName: string, data: Manifest): Promise<void> =>
|
|
28
|
-
manifestsResource.save(fileName, data);
|
|
29
|
-
export const deleteManifest = (fileName: string): Promise<void> =>
|
|
30
|
-
manifestsResource.remove(fileName);
|
|
31
|
-
export const getActiveManifest = (): Promise<Manifest | null> => manifestsResource.getActive();
|
|
32
|
-
export const setActiveManifest = (fileName: string): Promise<void> =>
|
|
33
|
-
manifestsResource.setActive(fileName);
|
|
34
|
-
|
|
35
|
-
export interface ApplyManifestResult {
|
|
36
|
-
ok: boolean;
|
|
37
|
-
manifest: Manifest;
|
|
38
|
-
theme: Theme;
|
|
39
|
-
componentConfigs: Record<string, ComponentConfig>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Server-side atomic apply: validate every referenced file exists, flip the
|
|
44
|
-
* theme + each component's `_active.json` and `_production.json` pointers,
|
|
45
|
-
* sync tokens.css/fonts.css from the new theme, mark the manifest active, and
|
|
46
|
-
* return the resolved theme + component configs in one payload. Clients
|
|
47
|
-
* usually follow with a full page reload — manifest load is a "blow up the
|
|
48
|
-
* world" action.
|
|
49
|
-
*/
|
|
50
|
-
export async function applyManifest(fileName: string): Promise<ApplyManifestResult> {
|
|
51
|
-
const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/apply`, {
|
|
52
|
-
method: 'PUT',
|
|
53
|
-
});
|
|
54
|
-
if (!res.ok) {
|
|
55
|
-
const err = await res.json().catch(() => ({ error: 'Apply failed' }));
|
|
56
|
-
throw new Error(err.error || 'Apply failed');
|
|
57
|
-
}
|
|
58
|
-
return res.json();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Snapshot the currently-active theme + component-config file pointers into
|
|
63
|
-
* a new manifest file and set it active. Used by the manifest panel's Save As
|
|
64
|
-
* action and by the SaveAs-then-Adopt recovery flow when active is `default`.
|
|
65
|
-
*/
|
|
66
|
-
export async function saveAsManifest(
|
|
67
|
-
fileName: string,
|
|
68
|
-
displayName: string,
|
|
69
|
-
): Promise<void> {
|
|
70
|
-
const activeTheme = await getActiveTheme();
|
|
71
|
-
if (!activeTheme || !activeTheme._fileName) {
|
|
72
|
-
throw new Error('No active theme on disk to capture');
|
|
73
|
-
}
|
|
74
|
-
const components = await listComponents();
|
|
75
|
-
const componentConfigs: Record<string, string> = {};
|
|
76
|
-
for (const c of components) {
|
|
77
|
-
componentConfigs[c.name] = c.activeFile || 'default';
|
|
78
|
-
}
|
|
79
|
-
const now = new Date().toISOString();
|
|
80
|
-
const manifest: Manifest = {
|
|
81
|
-
name: displayName,
|
|
82
|
-
createdAt: now,
|
|
83
|
-
updatedAt: now,
|
|
84
|
-
theme: activeTheme._fileName,
|
|
85
|
-
componentConfigs,
|
|
86
|
-
};
|
|
87
|
-
await saveManifest(fileName, manifest);
|
|
88
|
-
await setActiveManifest(fileName);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Re-snapshot the editor's current active pointers into the *currently active*
|
|
93
|
-
* manifest file. Used by the manifest panel's Save action. Server rejects with
|
|
94
|
-
* 403 if active is `default` (protected).
|
|
95
|
-
*/
|
|
96
|
-
export async function saveActiveManifest(displayName?: string): Promise<void> {
|
|
97
|
-
const active = await getActiveManifest();
|
|
98
|
-
if (!active || !active._fileName) {
|
|
99
|
-
throw new Error('No active manifest');
|
|
100
|
-
}
|
|
101
|
-
const activeTheme = await getActiveTheme();
|
|
102
|
-
if (!activeTheme || !activeTheme._fileName) {
|
|
103
|
-
throw new Error('No active theme on disk');
|
|
104
|
-
}
|
|
105
|
-
const components = await listComponents();
|
|
106
|
-
const componentConfigs: Record<string, string> = {};
|
|
107
|
-
for (const c of components) {
|
|
108
|
-
componentConfigs[c.name] = c.activeFile || 'default';
|
|
109
|
-
}
|
|
110
|
-
const manifest: Manifest = {
|
|
111
|
-
name: displayName ?? active.name,
|
|
112
|
-
createdAt: active.createdAt,
|
|
113
|
-
updatedAt: new Date().toISOString(),
|
|
114
|
-
theme: activeTheme._fileName,
|
|
115
|
-
componentConfigs,
|
|
116
|
-
};
|
|
117
|
-
await saveManifest(active._fileName, manifest);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface ImportManifestResult {
|
|
121
|
-
ok: boolean;
|
|
122
|
-
/** Final manifest filename (may be renamed if it collided with an existing one). */
|
|
123
|
-
manifest: string;
|
|
124
|
-
/** Keyed `theme:<orig>` / `componentConfig:<comp>/<orig>` / `manifest:<orig>` → final name. */
|
|
125
|
-
renames: Record<string, string>;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Fetch the manifest as a self-contained `ManifestBundle` and trigger a
|
|
130
|
-
* browser download. Hidden-anchor trick — no infrastructure beyond the
|
|
131
|
-
* existing GET `/api/manifests/:name/export` endpoint.
|
|
132
|
-
*
|
|
133
|
-
* See temp/manifest-robustness-plan.md §11.
|
|
134
|
-
*/
|
|
135
|
-
export async function exportManifest(fileName: string): Promise<void> {
|
|
136
|
-
const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/export`);
|
|
137
|
-
if (!res.ok) {
|
|
138
|
-
const err = await res.json().catch(() => ({ error: 'Export failed' }));
|
|
139
|
-
throw new Error(err.error || 'Export failed');
|
|
140
|
-
}
|
|
141
|
-
const blob = await res.blob();
|
|
142
|
-
const url = URL.createObjectURL(blob);
|
|
143
|
-
try {
|
|
144
|
-
const a = document.createElement('a');
|
|
145
|
-
a.href = url;
|
|
146
|
-
a.download = `${fileName}.bundle.json`;
|
|
147
|
-
document.body.appendChild(a);
|
|
148
|
-
a.click();
|
|
149
|
-
a.remove();
|
|
150
|
-
} finally {
|
|
151
|
-
URL.revokeObjectURL(url);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* POST a `ManifestBundle` to the import endpoint. Server materialises the
|
|
157
|
-
* inlined theme + component configs as fresh files (renaming on collision),
|
|
158
|
-
* rewrites the manifest's pointers, and returns the final manifest name plus
|
|
159
|
-
* the rename map so the UI can surface what got renamed.
|
|
160
|
-
*/
|
|
161
|
-
export async function importManifest(bundle: ManifestBundle): Promise<ImportManifestResult> {
|
|
162
|
-
const res = await fetch(`${API_BASE}/manifests/import`, {
|
|
163
|
-
method: 'POST',
|
|
164
|
-
headers: { 'Content-Type': 'application/json' },
|
|
165
|
-
body: JSON.stringify(bundle),
|
|
166
|
-
});
|
|
167
|
-
if (!res.ok) {
|
|
168
|
-
const err = await res.json().catch(() => ({ error: 'Import failed' }));
|
|
169
|
-
throw new Error(err.error || 'Import failed');
|
|
170
|
-
}
|
|
171
|
-
return res.json();
|
|
172
|
-
}
|
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
import type { ManifestMeta } from '../core/themes/themeTypes';
|
|
4
|
-
import {
|
|
5
|
-
listManifests,
|
|
6
|
-
deleteManifest,
|
|
7
|
-
getActiveManifest,
|
|
8
|
-
applyManifest,
|
|
9
|
-
saveAsManifest,
|
|
10
|
-
saveActiveManifest,
|
|
11
|
-
exportManifest,
|
|
12
|
-
importManifest,
|
|
13
|
-
} from '../core/manifests/manifestService';
|
|
14
|
-
import { dirty, componentDirty } from '../core/store/editorStore';
|
|
15
|
-
import { productionRevision, activeManifest } from '../core/productionPulse';
|
|
16
|
-
import { flashStatus } from '../core/flashStatus';
|
|
17
|
-
import UIInfoPopover from './UIInfoPopover.svelte';
|
|
18
|
-
import FileLoadList from './FileLoadList.svelte';
|
|
19
|
-
import FilePill from './FilePill.svelte';
|
|
20
|
-
import SaveAsDialog from '../component-editor/scaffolding/SaveAsDialog.svelte';
|
|
21
|
-
|
|
22
|
-
let files: ManifestMeta[] = $state([]);
|
|
23
|
-
let showFileList = $state(false);
|
|
24
|
-
let saveAsDialog = $state(false);
|
|
25
|
-
let saveStatus: 'idle' | 'saving' | 'saved' | 'error' = $state('idle');
|
|
26
|
-
|
|
27
|
-
let activeFileName = $state('default');
|
|
28
|
-
let currentDisplayName = $state('Default');
|
|
29
|
-
let activeIsProtected = $derived(activeFileName === 'default');
|
|
30
|
-
|
|
31
|
-
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
|
32
|
-
const setSaveStatus = (s: SaveState) => (saveStatus = s);
|
|
33
|
-
|
|
34
|
-
let dirtyComponentCount = $derived(
|
|
35
|
-
Object.values($componentDirty).filter(Boolean).length,
|
|
36
|
-
);
|
|
37
|
-
let editorDirty = $derived($dirty || dirtyComponentCount > 0);
|
|
38
|
-
|
|
39
|
-
async function refreshFiles() {
|
|
40
|
-
try {
|
|
41
|
-
files = await listManifests();
|
|
42
|
-
} catch {
|
|
43
|
-
// silent — empty list
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function refreshActive() {
|
|
48
|
-
try {
|
|
49
|
-
const active = await getActiveManifest();
|
|
50
|
-
if (active) {
|
|
51
|
-
activeFileName = active._fileName ?? 'default';
|
|
52
|
-
currentDisplayName = active.name ?? activeFileName;
|
|
53
|
-
const meta = (await listManifests()).find((f) => f.fileName === activeFileName) ?? null;
|
|
54
|
-
activeManifest.set(meta);
|
|
55
|
-
}
|
|
56
|
-
} catch {
|
|
57
|
-
// silent
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
onMount(async () => {
|
|
62
|
-
await refreshFiles();
|
|
63
|
-
await refreshActive();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Re-read active manifest whenever a sibling Adopt fires — the server
|
|
67
|
-
// patches our active file in those moments, so the timestamp + refs
|
|
68
|
-
// displayed here need to track. Skip the first tick (refreshActive ran
|
|
69
|
-
// already on mount).
|
|
70
|
-
let pulseInitialised = false;
|
|
71
|
-
$effect(() => {
|
|
72
|
-
void $productionRevision;
|
|
73
|
-
if (!pulseInitialised) {
|
|
74
|
-
pulseInitialised = true;
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
refreshActive();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// A manifest snapshots saved file pointers, not the editor's live state. Warn
|
|
81
|
-
// before capturing if there are unsaved theme/component edits, since those
|
|
82
|
-
// won't make it into the manifest until they're saved (and adopted).
|
|
83
|
-
function confirmUnsavedExclusion(): boolean {
|
|
84
|
-
if (!editorDirty) return true;
|
|
85
|
-
return window.confirm(
|
|
86
|
-
'You have unsaved theme or component changes. A manifest captures only saved, ' +
|
|
87
|
-
'adopted files, so these edits will not be included. Save and adopt them first ' +
|
|
88
|
-
'to capture them. Save the manifest anyway?',
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function handleSave() {
|
|
93
|
-
if (activeIsProtected) return;
|
|
94
|
-
if (!confirmUnsavedExclusion()) return;
|
|
95
|
-
saveStatus = 'saving';
|
|
96
|
-
try {
|
|
97
|
-
await saveActiveManifest(currentDisplayName);
|
|
98
|
-
await refreshActive();
|
|
99
|
-
flashStatus(setSaveStatus, 'saved');
|
|
100
|
-
} catch {
|
|
101
|
-
flashStatus(setSaveStatus, 'error');
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function openSaveAs() {
|
|
106
|
-
if (!confirmUnsavedExclusion()) return;
|
|
107
|
-
showFileList = false;
|
|
108
|
-
saveAsDialog = true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
112
|
-
saveStatus = 'saving';
|
|
113
|
-
try {
|
|
114
|
-
await saveAsManifest(detail.fileName, detail.displayName);
|
|
115
|
-
await refreshFiles();
|
|
116
|
-
await refreshActive();
|
|
117
|
-
flashStatus(setSaveStatus, 'saved');
|
|
118
|
-
} catch {
|
|
119
|
-
flashStatus(setSaveStatus, 'error');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function handleApply(file: ManifestMeta) {
|
|
124
|
-
if (editorDirty) {
|
|
125
|
-
const ok = window.confirm(
|
|
126
|
-
'Loading a manifest will reload the editor and discard unsaved changes. Continue?',
|
|
127
|
-
);
|
|
128
|
-
if (!ok) return;
|
|
129
|
-
}
|
|
130
|
-
showFileList = false;
|
|
131
|
-
try {
|
|
132
|
-
await applyManifest(file.fileName);
|
|
133
|
-
// applyManifest atomically flips active + production pointers and
|
|
134
|
-
// syncs tokens.css; reload to rehydrate the editor from the
|
|
135
|
-
// now-active theme + component configs.
|
|
136
|
-
window.location.reload();
|
|
137
|
-
} catch (err) {
|
|
138
|
-
window.alert(`Failed to apply manifest: ${(err as Error).message}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function handleExport(file: ManifestMeta) {
|
|
143
|
-
try {
|
|
144
|
-
await exportManifest(file.fileName);
|
|
145
|
-
} catch (err) {
|
|
146
|
-
window.alert(`Failed to export: ${(err as Error).message}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let importInput: HTMLInputElement | null = $state(null);
|
|
151
|
-
|
|
152
|
-
function openImport() {
|
|
153
|
-
importInput?.click();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function handleImportFile(event: Event) {
|
|
157
|
-
const input = event.target as HTMLInputElement;
|
|
158
|
-
const file = input.files?.[0];
|
|
159
|
-
input.value = ''; // allow re-picking the same file later
|
|
160
|
-
if (!file) return;
|
|
161
|
-
let bundle: any;
|
|
162
|
-
try {
|
|
163
|
-
bundle = JSON.parse(await file.text());
|
|
164
|
-
} catch {
|
|
165
|
-
window.alert('Selected file is not valid JSON.');
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (bundle?.kind !== 'manifest-bundle') {
|
|
169
|
-
window.alert('Not a manifest bundle (missing kind discriminator).');
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
try {
|
|
173
|
-
const result = await importManifest(bundle);
|
|
174
|
-
await refreshFiles();
|
|
175
|
-
const renameCount = Object.keys(result.renames).length;
|
|
176
|
-
if (renameCount > 0) {
|
|
177
|
-
const summary = Object.entries(result.renames)
|
|
178
|
-
.map(([k, v]) => `${k} → ${v}`)
|
|
179
|
-
.join('\n');
|
|
180
|
-
window.alert(
|
|
181
|
-
`Imported as "${result.manifest}". ${renameCount} file(s) renamed to avoid collisions:\n\n${summary}`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
} catch (err) {
|
|
185
|
-
window.alert(`Failed to import: ${(err as Error).message}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function handleDelete(file: ManifestMeta) {
|
|
190
|
-
if (file.isProtected) return;
|
|
191
|
-
if (file.fileName === activeFileName) {
|
|
192
|
-
window.alert('Cannot delete the active manifest. Load another manifest first.');
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const ok = window.confirm(`Delete manifest "${file.name}"?`);
|
|
196
|
-
if (!ok) return;
|
|
197
|
-
try {
|
|
198
|
-
await deleteManifest(file.fileName);
|
|
199
|
-
await refreshFiles();
|
|
200
|
-
} catch (err) {
|
|
201
|
-
window.alert(`Failed to delete: ${(err as Error).message}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function toggleFileList() {
|
|
206
|
-
showFileList = !showFileList;
|
|
207
|
-
if (showFileList) refreshFiles();
|
|
208
|
-
}
|
|
209
|
-
</script>
|
|
210
|
-
|
|
211
|
-
<div class="manifest-file-manager">
|
|
212
|
-
<div class="mfm-header">
|
|
213
|
-
<span class="mfm-header-label">Manifest</span>
|
|
214
|
-
<UIInfoPopover title="Manifests" ariaLabel="About manifests">
|
|
215
|
-
<p>
|
|
216
|
-
A <strong>manifest</strong> pins one theme plus one config file per component.
|
|
217
|
-
</p>
|
|
218
|
-
<p>
|
|
219
|
-
The <strong>active</strong> manifest is what the editor reads and what production runs. Theme and component <strong>Adopt</strong> actions auto-update its file.
|
|
220
|
-
</p>
|
|
221
|
-
<p>
|
|
222
|
-
<strong>Default</strong> is protected. To start customizing, <strong>Save As</strong> a new manifest first.
|
|
223
|
-
</p>
|
|
224
|
-
</UIInfoPopover>
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
<div class="mfm-card" class:protected={activeIsProtected}>
|
|
228
|
-
<span class="mfm-rail" aria-hidden="true"></span>
|
|
229
|
-
<div class="mfm-card-head">
|
|
230
|
-
<span class="mfm-card-label">Active</span>
|
|
231
|
-
{#if activeIsProtected}
|
|
232
|
-
<span class="mfm-badge protected" title="The default manifest is read-only">
|
|
233
|
-
<i class="fas fa-lock" aria-hidden="true"></i>
|
|
234
|
-
<span>protected</span>
|
|
235
|
-
</span>
|
|
236
|
-
{/if}
|
|
237
|
-
</div>
|
|
238
|
-
<FilePill
|
|
239
|
-
name={currentDisplayName}
|
|
240
|
-
isProtected={activeIsProtected}
|
|
241
|
-
protectedTitle="Protected default manifest"
|
|
242
|
-
title={currentDisplayName}
|
|
243
|
-
style="display: flex;"
|
|
244
|
-
/>
|
|
245
|
-
<div class="mfm-card-actions">
|
|
246
|
-
<button
|
|
247
|
-
class="mfm-btn mfm-btn-row save-btn"
|
|
248
|
-
class:saving={saveStatus === 'saving'}
|
|
249
|
-
class:saved={saveStatus === 'saved'}
|
|
250
|
-
class:error={saveStatus === 'error'}
|
|
251
|
-
onclick={handleSave}
|
|
252
|
-
disabled={activeIsProtected || saveStatus === 'saving'}
|
|
253
|
-
title={activeIsProtected
|
|
254
|
-
? 'Default is read-only — use Save As to capture under a new name'
|
|
255
|
-
: 'Re-stamp the active manifest with the current editor state'}
|
|
256
|
-
>
|
|
257
|
-
<i
|
|
258
|
-
class="fas"
|
|
259
|
-
class:fa-save={saveStatus === 'idle'}
|
|
260
|
-
class:fa-spinner={saveStatus === 'saving'}
|
|
261
|
-
class:fa-check={saveStatus === 'saved'}
|
|
262
|
-
class:fa-times={saveStatus === 'error'}
|
|
263
|
-
></i>
|
|
264
|
-
<span>
|
|
265
|
-
{#if saveStatus === 'idle'}Save{:else if saveStatus === 'saving'}Saving{:else if saveStatus === 'saved'}Saved{:else}Error{/if}
|
|
266
|
-
</span>
|
|
267
|
-
</button>
|
|
268
|
-
<button class="mfm-btn mfm-btn-row" onclick={openSaveAs} title="Save current state as a new manifest">
|
|
269
|
-
<i class="fas fa-copy"></i>
|
|
270
|
-
<span>Save As…</span>
|
|
271
|
-
</button>
|
|
272
|
-
<button
|
|
273
|
-
class="mfm-btn mfm-btn-row"
|
|
274
|
-
class:active={showFileList}
|
|
275
|
-
onclick={toggleFileList}
|
|
276
|
-
title="Load a manifest"
|
|
277
|
-
>
|
|
278
|
-
<i class="fas fa-folder-open"></i>
|
|
279
|
-
<span>Load…</span>
|
|
280
|
-
</button>
|
|
281
|
-
<button
|
|
282
|
-
class="mfm-btn mfm-btn-row"
|
|
283
|
-
onclick={openImport}
|
|
284
|
-
title="Import a shared manifest bundle"
|
|
285
|
-
>
|
|
286
|
-
<i class="fas fa-file-import"></i>
|
|
287
|
-
<span>Import…</span>
|
|
288
|
-
</button>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
|
|
293
|
-
<!-- Hidden file input for Import; clicked via openImport(). -->
|
|
294
|
-
<input
|
|
295
|
-
bind:this={importInput}
|
|
296
|
-
type="file"
|
|
297
|
-
accept=".json,application/json"
|
|
298
|
-
onchange={handleImportFile}
|
|
299
|
-
style="display: none;"
|
|
300
|
-
/>
|
|
301
|
-
|
|
302
|
-
<FileLoadList
|
|
303
|
-
bind:show={showFileList}
|
|
304
|
-
title="Load Manifest"
|
|
305
|
-
{files}
|
|
306
|
-
{activeFileName}
|
|
307
|
-
onload={handleApply}
|
|
308
|
-
ondelete={handleDelete}
|
|
309
|
-
onexport={handleExport}
|
|
310
|
-
exportTitle={(f) => `Export "${f.name}" as a shareable bundle`}
|
|
311
|
-
/>
|
|
312
|
-
|
|
313
|
-
<SaveAsDialog
|
|
314
|
-
bind:show={saveAsDialog}
|
|
315
|
-
{currentDisplayName}
|
|
316
|
-
{files}
|
|
317
|
-
title="Save Manifest As"
|
|
318
|
-
placeholder="Manifest name…"
|
|
319
|
-
reservedNameMessage='The name "default" is reserved for the protected baseline.'
|
|
320
|
-
branchFromDefaultName="my-manifest"
|
|
321
|
-
onsave={confirmSaveAs}
|
|
322
|
-
/>
|
|
323
|
-
|
|
324
|
-
<style>
|
|
325
|
-
.manifest-file-manager {
|
|
326
|
-
--mfm-active: #5aa85e;
|
|
327
|
-
--mfm-rail-neutral: var(--ui-border);
|
|
328
|
-
--mfm-rail-active: var(--mfm-active);
|
|
329
|
-
|
|
330
|
-
display: flex;
|
|
331
|
-
flex-direction: column;
|
|
332
|
-
gap: var(--ui-space-8);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
.mfm-header {
|
|
336
|
-
display: flex;
|
|
337
|
-
align-items: center;
|
|
338
|
-
justify-content: space-between;
|
|
339
|
-
gap: var(--ui-space-4);
|
|
340
|
-
padding: 0 var(--ui-space-4);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
.mfm-header-label {
|
|
344
|
-
font-size: var(--ui-font-size-xs);
|
|
345
|
-
color: var(--ui-text-secondary);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.mfm-card {
|
|
349
|
-
position: relative;
|
|
350
|
-
display: flex;
|
|
351
|
-
flex-direction: column;
|
|
352
|
-
gap: var(--ui-space-6);
|
|
353
|
-
padding: var(--ui-space-8) var(--ui-space-10) var(--ui-space-10) var(--ui-space-16);
|
|
354
|
-
background: var(--ui-surface-lower);
|
|
355
|
-
border: 1px solid var(--ui-border-low);
|
|
356
|
-
border-radius: var(--ui-radius-md);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
.mfm-rail {
|
|
360
|
-
position: absolute;
|
|
361
|
-
left: 0;
|
|
362
|
-
top: 0;
|
|
363
|
-
bottom: 0;
|
|
364
|
-
width: 3px;
|
|
365
|
-
border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
|
|
366
|
-
background: var(--mfm-rail-active);
|
|
367
|
-
transition: background var(--ui-transition-base);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.mfm-card.protected .mfm-rail {
|
|
371
|
-
background: var(--mfm-rail-neutral);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
.mfm-card-head {
|
|
375
|
-
display: flex;
|
|
376
|
-
align-items: baseline;
|
|
377
|
-
justify-content: space-between;
|
|
378
|
-
gap: var(--ui-space-8);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
.mfm-card-label {
|
|
382
|
-
font-size: var(--ui-font-size-xs);
|
|
383
|
-
font-weight: var(--ui-font-weight-semibold);
|
|
384
|
-
color: var(--ui-text-tertiary);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.mfm-badge {
|
|
388
|
-
display: inline-flex;
|
|
389
|
-
align-items: center;
|
|
390
|
-
gap: var(--ui-space-4);
|
|
391
|
-
font-size: var(--ui-font-size-xs);
|
|
392
|
-
color: var(--ui-text-tertiary);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
.mfm-badge.protected i {
|
|
396
|
-
font-size: 0.8em;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
.mfm-card-actions {
|
|
400
|
-
display: flex;
|
|
401
|
-
flex-direction: column;
|
|
402
|
-
gap: var(--ui-space-4);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
.mfm-btn {
|
|
406
|
-
display: inline-flex;
|
|
407
|
-
align-items: center;
|
|
408
|
-
justify-content: flex-start;
|
|
409
|
-
gap: var(--ui-space-8);
|
|
410
|
-
padding: var(--ui-space-6) var(--ui-space-10);
|
|
411
|
-
background: var(--ui-surface-high);
|
|
412
|
-
border: 1px solid var(--ui-border-low);
|
|
413
|
-
border-radius: var(--ui-radius-sm);
|
|
414
|
-
color: var(--ui-text-primary);
|
|
415
|
-
font-family: inherit;
|
|
416
|
-
font-size: var(--ui-font-size-sm);
|
|
417
|
-
line-height: 1;
|
|
418
|
-
cursor: pointer;
|
|
419
|
-
transition: background var(--ui-transition-fast), color var(--ui-transition-fast);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
.mfm-btn:hover:not(:disabled) {
|
|
423
|
-
background: var(--ui-hover);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
.mfm-btn:disabled {
|
|
427
|
-
cursor: not-allowed;
|
|
428
|
-
opacity: 0.5;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
.mfm-btn.active {
|
|
432
|
-
background: var(--ui-active);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
.mfm-btn.saved { color: var(--mfm-active); }
|
|
436
|
-
.mfm-btn.error { color: var(--ui-error, #c0392b); }
|
|
437
|
-
|
|
438
|
-
.mfm-btn .fa-spinner {
|
|
439
|
-
animation: mfm-spin 0.8s linear infinite;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
@keyframes mfm-spin {
|
|
443
|
-
from { transform: rotate(0deg); }
|
|
444
|
-
to { transform: rotate(360deg); }
|
|
445
|
-
}
|
|
446
|
-
</style>
|