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