@motion-proto/live-tokens 0.48.1 → 0.49.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/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/bin/generate-theme.mjs +10 -17
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/index.cjs +81 -13
- package/dist-plugin/index.js +81 -13
- package/dist-plugin/{themeTypes-DMHZOnUn.d.cts → themeTypes-DSwKq-bj.d.cts} +2 -3
- package/dist-plugin/{themeTypes-DMHZOnUn.d.ts → themeTypes-DSwKq-bj.d.ts} +2 -3
- package/package.json +1 -1
- package/src/editor/core/themes/themeService.ts +3 -3
- package/src/editor/core/themes/themeTypes.ts +6 -6
- package/src/editor/docs/content/themes-workflow.md +5 -4
- package/src/editor/docs/content/where-themes-live.md +6 -5
- package/src/editor/docs/content.generated.ts +2 -2
- package/src/editor/ui/ThemePanel.svelte +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.49.0 — Working buffers are active-theme deltas
|
|
4
|
+
|
|
5
|
+
### Changed (breaking)
|
|
6
|
+
|
|
7
|
+
- **Working buffers are deltas from the active theme.** Loading a theme now
|
|
8
|
+
clears working buffers and changes only `themes/_active.json`; live reads
|
|
9
|
+
resolve through the active theme. Saving removes buffers whose content is now
|
|
10
|
+
durable, while deleting an active theme materialises only the deltas needed
|
|
11
|
+
to preserve the visible look.
|
|
12
|
+
|
|
3
13
|
## 0.48.1 — Demo typography follows the theme
|
|
4
14
|
|
|
5
15
|
### Fixed
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A foundational design system for quickly styling and building Svelte + Vite micr
|
|
|
11
11
|
- **Theme editor** (`/live-tokens/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, then Adopt one to bake it into static CSS for the build.
|
|
12
12
|
- **Per-component editor** (`/live-tokens/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
|
|
13
13
|
- **Live editor overlay** — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit *on the page you're styling*, not in a separate tab. Includes a "Page Source" button that opens the current page's `.svelte` file in VS Code.
|
|
14
|
-
- **Themes** — a theme is a whole look in one file: colors and type plus a config for every component you changed, held by value. In the editor this is the Theme panel, with Colors & Type and Components as its parts. Themes are documents: loading one opens it
|
|
14
|
+
- **Themes** — a theme is a whole look in one file: colors and type plus a config for every component you changed, held by value. In the editor this is the Theme panel, with Colors & Type and Components as its parts. Themes are documents: loading one opens it with a one-file `_active.json` pointer change, live reads fall through to its content, and nothing your site ships changes until you Adopt. A narrower load takes the colors and type alone and leaves your shapes. Export one and import it into another project to restore the full styling in one step.
|
|
15
15
|
- **Seven example looks** — Autumn, Halloween, Midnight Study, Ocean, Royal Velvet, Spring Meadow and Sunset each ship as a full theme: preset colors and type plus a shape personality of radius, padding, gap and border-width aliases. Each preset also names its own Google Fonts pairing, one display family and one body family, so a look carries type as well as colour and shape. They need no local files, so Load one to try a whole look on your own pages and load Motion Proto to come back. Saving over a preset writes a local copy that shadows the shipped one; delete that copy and the shipped version returns.
|
|
16
16
|
- **Vite plugin** — hosts the `/api/live-tokens/{colors-and-type,component-configs,themes}/*` routes the editor reads and saves through. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
|
|
17
17
|
- **Claude Code skill suite** — five bundled skills so you can drive the package in plain English. `build-page` composes pages from the shipped components. `pick-component` decides between confusing pairs (TabBar vs SegmentedControl, Card vs CollapsibleSection). `create-component` authors a new editable component against the project's naming, state-model, and import rules. `generate-theme` turns a mood brief ("bright and cheerful", "dark night theme") into a complete AA-checked color theme. `adjust-shape-space` turns "make the buttons pill shaped" or "space it out" into new radius, padding, gap, and border-width aliases. One command to install them all: `npx @motion-proto/live-tokens setup-claude`. See [Claude Code skills](#claude-code-skills) below.
|
|
@@ -56,7 +56,7 @@ A project last opened on 0.47.1 or earlier still keeps its colors and type in `t
|
|
|
56
56
|
|
|
57
57
|
By default, the plugin reads and writes under one folder: `src/live-tokens/data/`. Inside that folder live three subdirectories — `colors-and-type/`, `themes/`, `component-configs/` — each owned by the plugin.
|
|
58
58
|
|
|
59
|
-
`themes/` holds the documents: one file per whole look, plus `_active.json` naming the one the editor has open and `_production.json` naming the one your site ships. `colors-and-type/` and `component-configs/{comp}/` hold each layer's `default.json` baseline, any preset you save by name, and the `_working.json` buffer for edits you have not saved into
|
|
59
|
+
`themes/` holds the documents: one file per whole look, plus `_active.json` naming the one the editor has open and `_production.json` naming the one your site ships. `colors-and-type/` and `component-configs/{comp}/` hold each layer's `default.json` baseline, any preset you save by name, and the `_working.json` buffer for edits you have not saved into the active theme. A buffer is a delta from that open document, so ordinary theme switching leaves none.
|
|
60
60
|
|
|
61
61
|
To move them, create a `live-tokens.config.json` at your project root:
|
|
62
62
|
|
package/bin/generate-theme.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// TS sources), enforces the AA contrast gate, and saves the result as a theme:
|
|
6
6
|
// <themesDir>/<slug>.json, the document that carries the whole look by value.
|
|
7
7
|
// Unless --no-activate it then opens that theme the way the dev server's apply
|
|
8
|
-
// door does —
|
|
8
|
+
// door does — existing `_working` buffers are cleared and
|
|
9
9
|
// `themes/_active.json` names it. Nothing else moves, so generating a theme
|
|
10
10
|
// cannot change what the site ships.
|
|
11
11
|
//
|
|
@@ -97,9 +97,8 @@ function resolveCarrySource({ carryFrom, colorsAndTypeDir, componentConfigsDir,
|
|
|
97
97
|
readData(colorsAndTypeDir, 'colors-and-type', 'default'),
|
|
98
98
|
);
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// components it changes.
|
|
100
|
+
// A working buffer is an unsaved delta from the active theme; absent theme
|
|
101
|
+
// component entries fall through to the component defaults.
|
|
103
102
|
const componentConfigs = {};
|
|
104
103
|
for (const comp of comps) {
|
|
105
104
|
const live =
|
|
@@ -168,7 +167,7 @@ export async function runGenerateTheme({
|
|
|
168
167
|
if (!dryRun) {
|
|
169
168
|
mkdirSync(dirs.themesDir, { recursive: true });
|
|
170
169
|
writeFileSync(themePath, JSON.stringify(theme, null, 2) + '\n');
|
|
171
|
-
if (activate) applyTheme(
|
|
170
|
+
if (activate) applyTheme(slug, dirs);
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
return {
|
|
@@ -185,21 +184,15 @@ export async function runGenerateTheme({
|
|
|
185
184
|
};
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
/** The apply door's write set, reproduced:
|
|
189
|
-
*
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
writeFileSync(
|
|
194
|
-
join(dirs.colorsAndTypeDir, '_working.json'),
|
|
195
|
-
JSON.stringify(theme.colorsAndType, null, 2),
|
|
196
|
-
);
|
|
187
|
+
/** The apply door's write set, reproduced: clear every working delta and point
|
|
188
|
+
* `themes/_active.json` at the open document. Production is untouched. */
|
|
189
|
+
function applyTheme(slug, dirs) {
|
|
190
|
+
const colorsWorking = join(dirs.colorsAndTypeDir, '_working.json');
|
|
191
|
+
if (existsSync(colorsWorking)) rmSync(colorsWorking);
|
|
197
192
|
|
|
198
193
|
for (const comp of componentNames(dirs.componentConfigsDir)) {
|
|
199
194
|
const workingPath = join(dirs.componentConfigsDir, comp, '_working.json');
|
|
200
|
-
|
|
201
|
-
if (embedded) writeFileSync(workingPath, JSON.stringify({ ...embedded, component: comp }, null, 2));
|
|
202
|
-
else if (existsSync(workingPath)) rmSync(workingPath);
|
|
195
|
+
if (existsSync(workingPath)) rmSync(workingPath);
|
|
203
196
|
}
|
|
204
197
|
|
|
205
198
|
writeFileSync(join(dirs.themesDir, '_active.json'), JSON.stringify({ activeFile: slug }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-
|
|
1
|
+
import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSwKq-bj.cjs';
|
|
2
2
|
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.cjs';
|
|
3
3
|
|
|
4
4
|
type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-
|
|
1
|
+
import { A as AliasDiskValue, a as ComponentConfig } from '../themeTypes-DSwKq-bj.js';
|
|
2
2
|
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.js';
|
|
3
3
|
|
|
4
4
|
type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as FontSource, b as FontStack, G as GradientDiskToken, O as Oklch, H as HarmonyMode, C as ColorsAndType } from '../themeTypes-
|
|
1
|
+
import { F as FontSource, b as FontStack, G as GradientDiskToken, O as Oklch, H as HarmonyMode, C as ColorsAndType } from '../themeTypes-DSwKq-bj.cjs';
|
|
2
2
|
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as FontSource, b as FontStack, G as GradientDiskToken, O as Oklch, H as HarmonyMode, C as ColorsAndType } from '../themeTypes-
|
|
1
|
+
import { F as FontSource, b as FontStack, G as GradientDiskToken, O as Oklch, H as HarmonyMode, C as ColorsAndType } from '../themeTypes-DSwKq-bj.js';
|
|
2
2
|
export { r as readLiveTokensConfig, a as resolveDataDirs } from '../dataPaths-DBN0RPuT.js';
|
|
3
3
|
|
|
4
4
|
/**
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -1931,6 +1931,50 @@ ${lines.join("\n")}
|
|
|
1931
1931
|
if (!cfg) return null;
|
|
1932
1932
|
return { data: cfg, source: "default" };
|
|
1933
1933
|
}
|
|
1934
|
+
function resolveSavedColorsAndType(theme) {
|
|
1935
|
+
if (theme?.colorsAndType) return theme.colorsAndType;
|
|
1936
|
+
const shipped = colorsAndTypeResource.readJson("default");
|
|
1937
|
+
return shipped ? normalizeColorsAndType(shipped) : null;
|
|
1938
|
+
}
|
|
1939
|
+
function resolveSavedComponentConfig(comp, theme) {
|
|
1940
|
+
const embedded = theme?.componentConfigs[comp];
|
|
1941
|
+
if (embedded) return { ...embedded, component: comp };
|
|
1942
|
+
return readComponentConfig(comp, "default");
|
|
1943
|
+
}
|
|
1944
|
+
function sameJsonValue(a, b) {
|
|
1945
|
+
if (a === null || b === null) return a === b;
|
|
1946
|
+
if (typeof a !== typeof b) return false;
|
|
1947
|
+
if (typeof a !== "object") return a === b;
|
|
1948
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1949
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, i) => sameJsonValue(value, b[i]));
|
|
1950
|
+
}
|
|
1951
|
+
const aRecord = a;
|
|
1952
|
+
const bRecord = b;
|
|
1953
|
+
const aKeys = Object.keys(aRecord).sort();
|
|
1954
|
+
const bKeys = Object.keys(bRecord).sort();
|
|
1955
|
+
return aKeys.length === bKeys.length && aKeys.every((key, i) => key === bKeys[i] && sameJsonValue(aRecord[key], bRecord[key]));
|
|
1956
|
+
}
|
|
1957
|
+
function pruneMatchingWorking(theme) {
|
|
1958
|
+
const colorsWorking = colorsAndTypeResource.readWorking();
|
|
1959
|
+
const savedColors = resolveSavedColorsAndType(theme);
|
|
1960
|
+
if (colorsWorking !== null && savedColors !== null && sameJsonValue(normalizeColorsAndType(colorsWorking), savedColors)) {
|
|
1961
|
+
colorsAndTypeResource.clearWorking();
|
|
1962
|
+
}
|
|
1963
|
+
for (const comp of listComponentNames()) {
|
|
1964
|
+
const r = componentResource(comp);
|
|
1965
|
+
const working = r.readWorking();
|
|
1966
|
+
if (working !== null && sameJsonValue(working, resolveSavedComponentConfig(comp, theme))) {
|
|
1967
|
+
r.clearWorking();
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function clearAllWorking() {
|
|
1972
|
+
colorsAndTypeResource.clearWorking();
|
|
1973
|
+
if (!import_fs4.default.existsSync(COMPONENT_CONFIGS_DIR)) return;
|
|
1974
|
+
for (const entry of import_fs4.default.readdirSync(COMPONENT_CONFIGS_DIR, { withFileTypes: true })) {
|
|
1975
|
+
if (entry.isDirectory()) componentResource(entry.name).clearWorking();
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1934
1978
|
function formatAliasGradient(v) {
|
|
1935
1979
|
const stopColor = (s) => {
|
|
1936
1980
|
const base = s.color.startsWith("--") ? `var(${s.color})` : s.color;
|
|
@@ -2037,7 +2081,10 @@ ${lines.join("\n")}
|
|
|
2037
2081
|
async function handleSetWorkingColorsAndType({ req, res }) {
|
|
2038
2082
|
const body = await readBufferBody(req, res);
|
|
2039
2083
|
if (!body) return;
|
|
2040
|
-
|
|
2084
|
+
const normalized = normalizeColorsAndType(body);
|
|
2085
|
+
const saved = resolveSavedColorsAndType(activeTheme());
|
|
2086
|
+
if (saved !== null && sameJsonValue(normalized, saved)) colorsAndTypeResource.clearWorking();
|
|
2087
|
+
else colorsAndTypeResource.writeWorking(body);
|
|
2041
2088
|
jsonResponse(res, 200, { ok: true });
|
|
2042
2089
|
}
|
|
2043
2090
|
async function handleClearWorkingColorsAndType({ res }) {
|
|
@@ -2139,7 +2186,9 @@ ${lines.join("\n")}
|
|
|
2139
2186
|
if (rejectUnknownComponent(res, comp)) return;
|
|
2140
2187
|
const body = await readBufferBody(req, res);
|
|
2141
2188
|
if (!body) return;
|
|
2142
|
-
componentResource(comp)
|
|
2189
|
+
const r = componentResource(comp);
|
|
2190
|
+
if (sameJsonValue(body, resolveSavedComponentConfig(comp, activeTheme()))) r.clearWorking();
|
|
2191
|
+
else r.writeWorking(body);
|
|
2143
2192
|
jsonResponse(res, 200, { ok: true });
|
|
2144
2193
|
}
|
|
2145
2194
|
async function handleClearComponentWorking({ params, res }) {
|
|
@@ -2261,6 +2310,7 @@ ${lines.join("\n")}
|
|
|
2261
2310
|
return;
|
|
2262
2311
|
}
|
|
2263
2312
|
themesResource.setActiveName(fileName);
|
|
2313
|
+
pruneMatchingWorking(readTheme(fileName)?.theme ?? null);
|
|
2264
2314
|
jsonResponse(res, 200, { ok: true, activeFile: fileName });
|
|
2265
2315
|
}
|
|
2266
2316
|
async function handleThemeByName({ params, req, res }) {
|
|
@@ -2292,6 +2342,7 @@ ${lines.join("\n")}
|
|
|
2292
2342
|
}
|
|
2293
2343
|
}
|
|
2294
2344
|
writeTheme(fileName, theme);
|
|
2345
|
+
if (themesResource.getActiveName() === fileName) pruneMatchingWorking(theme);
|
|
2295
2346
|
jsonResponse(res, 200, { ok: true, fileName });
|
|
2296
2347
|
return;
|
|
2297
2348
|
}
|
|
@@ -2308,10 +2359,35 @@ ${lines.join("\n")}
|
|
|
2308
2359
|
return;
|
|
2309
2360
|
}
|
|
2310
2361
|
if (import_fs4.default.existsSync(filePath)) {
|
|
2362
|
+
const deletingActive = themesResource.getActiveName() === fileName;
|
|
2363
|
+
const liveColors = deletingActive ? resolveLiveColorsAndType(activeTheme()) : null;
|
|
2364
|
+
const liveComponents = {};
|
|
2365
|
+
if (deletingActive) {
|
|
2366
|
+
for (const comp of listComponentNames()) {
|
|
2367
|
+
const resolved = resolveLiveComponentConfig(comp, activeTheme());
|
|
2368
|
+
if (resolved) liveComponents[comp] = resolved.data;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2311
2371
|
import_fs4.default.unlinkSync(filePath);
|
|
2312
|
-
if (themesResource.existingPath(fileName) === null &&
|
|
2372
|
+
if (themesResource.existingPath(fileName) === null && deletingActive) {
|
|
2313
2373
|
themesResource.setActiveName("default");
|
|
2314
2374
|
}
|
|
2375
|
+
if (deletingActive) {
|
|
2376
|
+
const replacement = activeTheme();
|
|
2377
|
+
const savedColors = resolveSavedColorsAndType(replacement);
|
|
2378
|
+
if (liveColors && !sameJsonValue(liveColors.data, savedColors)) {
|
|
2379
|
+
colorsAndTypeResource.writeWorking(liveColors.data);
|
|
2380
|
+
} else {
|
|
2381
|
+
colorsAndTypeResource.clearWorking();
|
|
2382
|
+
}
|
|
2383
|
+
for (const comp of listComponentNames()) {
|
|
2384
|
+
const r = componentResource(comp);
|
|
2385
|
+
const live = liveComponents[comp] ?? null;
|
|
2386
|
+
const saved = resolveSavedComponentConfig(comp, replacement);
|
|
2387
|
+
if (live !== null && !sameJsonValue(live, saved)) r.writeWorking({ ...live });
|
|
2388
|
+
else r.clearWorking();
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2315
2391
|
} else if (themesResource.existingPath(fileName)) {
|
|
2316
2392
|
jsonResponse(res, 403, {
|
|
2317
2393
|
error: "Cannot delete a theme shipped with the package. Saving it creates a local copy; deleting that copy restores the shipped version.",
|
|
@@ -2332,20 +2408,12 @@ ${lines.join("\n")}
|
|
|
2332
2408
|
return;
|
|
2333
2409
|
}
|
|
2334
2410
|
const { theme } = read;
|
|
2335
|
-
|
|
2336
|
-
if (!isDefault && !theme.colorsAndType) {
|
|
2411
|
+
if (!theme.colorsAndType) {
|
|
2337
2412
|
jsonResponse(res, 422, { error: "This theme carries no colors and type" });
|
|
2338
2413
|
return;
|
|
2339
2414
|
}
|
|
2340
|
-
|
|
2341
|
-
else colorsAndTypeResource.writeWorking(theme.colorsAndType);
|
|
2415
|
+
clearAllWorking();
|
|
2342
2416
|
const knownComponents = listComponentNames();
|
|
2343
|
-
for (const comp of knownComponents) {
|
|
2344
|
-
const embedded = isDefault ? void 0 : theme.componentConfigs[comp];
|
|
2345
|
-
const r = componentResource(comp);
|
|
2346
|
-
if (embedded) r.writeWorking({ ...embedded, component: comp });
|
|
2347
|
-
else r.clearWorking();
|
|
2348
|
-
}
|
|
2349
2417
|
themesResource.setActiveName(fileName);
|
|
2350
2418
|
const resolvedConfigs = {};
|
|
2351
2419
|
for (const comp of knownComponents) {
|
package/dist-plugin/index.js
CHANGED
|
@@ -656,6 +656,50 @@ ${lines.join("\n")}
|
|
|
656
656
|
if (!cfg) return null;
|
|
657
657
|
return { data: cfg, source: "default" };
|
|
658
658
|
}
|
|
659
|
+
function resolveSavedColorsAndType(theme) {
|
|
660
|
+
if (theme?.colorsAndType) return theme.colorsAndType;
|
|
661
|
+
const shipped = colorsAndTypeResource.readJson("default");
|
|
662
|
+
return shipped ? normalizeColorsAndType(shipped) : null;
|
|
663
|
+
}
|
|
664
|
+
function resolveSavedComponentConfig(comp, theme) {
|
|
665
|
+
const embedded = theme?.componentConfigs[comp];
|
|
666
|
+
if (embedded) return { ...embedded, component: comp };
|
|
667
|
+
return readComponentConfig(comp, "default");
|
|
668
|
+
}
|
|
669
|
+
function sameJsonValue(a, b) {
|
|
670
|
+
if (a === null || b === null) return a === b;
|
|
671
|
+
if (typeof a !== typeof b) return false;
|
|
672
|
+
if (typeof a !== "object") return a === b;
|
|
673
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
674
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, i) => sameJsonValue(value, b[i]));
|
|
675
|
+
}
|
|
676
|
+
const aRecord = a;
|
|
677
|
+
const bRecord = b;
|
|
678
|
+
const aKeys = Object.keys(aRecord).sort();
|
|
679
|
+
const bKeys = Object.keys(bRecord).sort();
|
|
680
|
+
return aKeys.length === bKeys.length && aKeys.every((key, i) => key === bKeys[i] && sameJsonValue(aRecord[key], bRecord[key]));
|
|
681
|
+
}
|
|
682
|
+
function pruneMatchingWorking(theme) {
|
|
683
|
+
const colorsWorking = colorsAndTypeResource.readWorking();
|
|
684
|
+
const savedColors = resolveSavedColorsAndType(theme);
|
|
685
|
+
if (colorsWorking !== null && savedColors !== null && sameJsonValue(normalizeColorsAndType(colorsWorking), savedColors)) {
|
|
686
|
+
colorsAndTypeResource.clearWorking();
|
|
687
|
+
}
|
|
688
|
+
for (const comp of listComponentNames()) {
|
|
689
|
+
const r = componentResource(comp);
|
|
690
|
+
const working = r.readWorking();
|
|
691
|
+
if (working !== null && sameJsonValue(working, resolveSavedComponentConfig(comp, theme))) {
|
|
692
|
+
r.clearWorking();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function clearAllWorking() {
|
|
697
|
+
colorsAndTypeResource.clearWorking();
|
|
698
|
+
if (!fs.existsSync(COMPONENT_CONFIGS_DIR)) return;
|
|
699
|
+
for (const entry of fs.readdirSync(COMPONENT_CONFIGS_DIR, { withFileTypes: true })) {
|
|
700
|
+
if (entry.isDirectory()) componentResource(entry.name).clearWorking();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
659
703
|
function formatAliasGradient(v) {
|
|
660
704
|
const stopColor = (s) => {
|
|
661
705
|
const base = s.color.startsWith("--") ? `var(${s.color})` : s.color;
|
|
@@ -762,7 +806,10 @@ ${lines.join("\n")}
|
|
|
762
806
|
async function handleSetWorkingColorsAndType({ req, res }) {
|
|
763
807
|
const body = await readBufferBody(req, res);
|
|
764
808
|
if (!body) return;
|
|
765
|
-
|
|
809
|
+
const normalized = normalizeColorsAndType(body);
|
|
810
|
+
const saved = resolveSavedColorsAndType(activeTheme());
|
|
811
|
+
if (saved !== null && sameJsonValue(normalized, saved)) colorsAndTypeResource.clearWorking();
|
|
812
|
+
else colorsAndTypeResource.writeWorking(body);
|
|
766
813
|
jsonResponse(res, 200, { ok: true });
|
|
767
814
|
}
|
|
768
815
|
async function handleClearWorkingColorsAndType({ res }) {
|
|
@@ -864,7 +911,9 @@ ${lines.join("\n")}
|
|
|
864
911
|
if (rejectUnknownComponent(res, comp)) return;
|
|
865
912
|
const body = await readBufferBody(req, res);
|
|
866
913
|
if (!body) return;
|
|
867
|
-
componentResource(comp)
|
|
914
|
+
const r = componentResource(comp);
|
|
915
|
+
if (sameJsonValue(body, resolveSavedComponentConfig(comp, activeTheme()))) r.clearWorking();
|
|
916
|
+
else r.writeWorking(body);
|
|
868
917
|
jsonResponse(res, 200, { ok: true });
|
|
869
918
|
}
|
|
870
919
|
async function handleClearComponentWorking({ params, res }) {
|
|
@@ -986,6 +1035,7 @@ ${lines.join("\n")}
|
|
|
986
1035
|
return;
|
|
987
1036
|
}
|
|
988
1037
|
themesResource.setActiveName(fileName);
|
|
1038
|
+
pruneMatchingWorking(readTheme(fileName)?.theme ?? null);
|
|
989
1039
|
jsonResponse(res, 200, { ok: true, activeFile: fileName });
|
|
990
1040
|
}
|
|
991
1041
|
async function handleThemeByName({ params, req, res }) {
|
|
@@ -1017,6 +1067,7 @@ ${lines.join("\n")}
|
|
|
1017
1067
|
}
|
|
1018
1068
|
}
|
|
1019
1069
|
writeTheme(fileName, theme);
|
|
1070
|
+
if (themesResource.getActiveName() === fileName) pruneMatchingWorking(theme);
|
|
1020
1071
|
jsonResponse(res, 200, { ok: true, fileName });
|
|
1021
1072
|
return;
|
|
1022
1073
|
}
|
|
@@ -1033,10 +1084,35 @@ ${lines.join("\n")}
|
|
|
1033
1084
|
return;
|
|
1034
1085
|
}
|
|
1035
1086
|
if (fs.existsSync(filePath)) {
|
|
1087
|
+
const deletingActive = themesResource.getActiveName() === fileName;
|
|
1088
|
+
const liveColors = deletingActive ? resolveLiveColorsAndType(activeTheme()) : null;
|
|
1089
|
+
const liveComponents = {};
|
|
1090
|
+
if (deletingActive) {
|
|
1091
|
+
for (const comp of listComponentNames()) {
|
|
1092
|
+
const resolved = resolveLiveComponentConfig(comp, activeTheme());
|
|
1093
|
+
if (resolved) liveComponents[comp] = resolved.data;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1036
1096
|
fs.unlinkSync(filePath);
|
|
1037
|
-
if (themesResource.existingPath(fileName) === null &&
|
|
1097
|
+
if (themesResource.existingPath(fileName) === null && deletingActive) {
|
|
1038
1098
|
themesResource.setActiveName("default");
|
|
1039
1099
|
}
|
|
1100
|
+
if (deletingActive) {
|
|
1101
|
+
const replacement = activeTheme();
|
|
1102
|
+
const savedColors = resolveSavedColorsAndType(replacement);
|
|
1103
|
+
if (liveColors && !sameJsonValue(liveColors.data, savedColors)) {
|
|
1104
|
+
colorsAndTypeResource.writeWorking(liveColors.data);
|
|
1105
|
+
} else {
|
|
1106
|
+
colorsAndTypeResource.clearWorking();
|
|
1107
|
+
}
|
|
1108
|
+
for (const comp of listComponentNames()) {
|
|
1109
|
+
const r = componentResource(comp);
|
|
1110
|
+
const live = liveComponents[comp] ?? null;
|
|
1111
|
+
const saved = resolveSavedComponentConfig(comp, replacement);
|
|
1112
|
+
if (live !== null && !sameJsonValue(live, saved)) r.writeWorking({ ...live });
|
|
1113
|
+
else r.clearWorking();
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1040
1116
|
} else if (themesResource.existingPath(fileName)) {
|
|
1041
1117
|
jsonResponse(res, 403, {
|
|
1042
1118
|
error: "Cannot delete a theme shipped with the package. Saving it creates a local copy; deleting that copy restores the shipped version.",
|
|
@@ -1057,20 +1133,12 @@ ${lines.join("\n")}
|
|
|
1057
1133
|
return;
|
|
1058
1134
|
}
|
|
1059
1135
|
const { theme } = read;
|
|
1060
|
-
|
|
1061
|
-
if (!isDefault && !theme.colorsAndType) {
|
|
1136
|
+
if (!theme.colorsAndType) {
|
|
1062
1137
|
jsonResponse(res, 422, { error: "This theme carries no colors and type" });
|
|
1063
1138
|
return;
|
|
1064
1139
|
}
|
|
1065
|
-
|
|
1066
|
-
else colorsAndTypeResource.writeWorking(theme.colorsAndType);
|
|
1140
|
+
clearAllWorking();
|
|
1067
1141
|
const knownComponents = listComponentNames();
|
|
1068
|
-
for (const comp of knownComponents) {
|
|
1069
|
-
const embedded = isDefault ? void 0 : theme.componentConfigs[comp];
|
|
1070
|
-
const r = componentResource(comp);
|
|
1071
|
-
if (embedded) r.writeWorking({ ...embedded, component: comp });
|
|
1072
|
-
else r.clearWorking();
|
|
1073
|
-
}
|
|
1074
1142
|
themesResource.setActiveName(fileName);
|
|
1075
1143
|
const resolvedConfigs = {};
|
|
1076
1144
|
for (const comp of knownComponents) {
|
|
@@ -134,9 +134,8 @@ interface GradientDiskToken {
|
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
136
|
* Where a live read resolved from: the unsaved `_working` buffer, the open
|
|
137
|
-
* theme's embedded copy, or the shipped default.
|
|
138
|
-
*
|
|
139
|
-
* every layer it carries.
|
|
137
|
+
* theme's embedded copy, or the shipped default. A `working` source is an
|
|
138
|
+
* unsaved delta from the active theme.
|
|
140
139
|
*/
|
|
141
140
|
type LiveSource = 'working' | 'theme' | 'default';
|
|
142
141
|
interface ColorsAndType {
|
|
@@ -134,9 +134,8 @@ interface GradientDiskToken {
|
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
136
|
* Where a live read resolved from: the unsaved `_working` buffer, the open
|
|
137
|
-
* theme's embedded copy, or the shipped default.
|
|
138
|
-
*
|
|
139
|
-
* every layer it carries.
|
|
137
|
+
* theme's embedded copy, or the shipped default. A `working` source is an
|
|
138
|
+
* unsaved delta from the active theme.
|
|
140
139
|
*/
|
|
141
140
|
type LiveSource = 'working' | 'theme' | 'default';
|
|
142
141
|
interface ColorsAndType {
|
package/package.json
CHANGED
|
@@ -63,9 +63,9 @@ export interface ApplyThemeResult {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Open a theme: the server
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* Open a theme: the server clears the `_working` buffers, points
|
|
67
|
+
* `themes/_active.json` at it, and returns the resolved state in one payload.
|
|
68
|
+
* Live reads then fall through to the theme's embedded layers.
|
|
69
69
|
* Production is untouched, so trying a look cannot change what the site ships.
|
|
70
70
|
* Clients follow with a full page reload; opening a theme is a "blow up the
|
|
71
71
|
* world" action.
|
|
@@ -112,9 +112,8 @@ export interface GradientDiskToken {
|
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Where a live read resolved from: the unsaved `_working` buffer, the open
|
|
115
|
-
* theme's embedded copy, or the shipped default.
|
|
116
|
-
*
|
|
117
|
-
* every layer it carries.
|
|
115
|
+
* theme's embedded copy, or the shipped default. A `working` source is an
|
|
116
|
+
* unsaved delta from the active theme.
|
|
118
117
|
*/
|
|
119
118
|
export type LiveSource = 'working' | 'theme' | 'default';
|
|
120
119
|
|
|
@@ -194,9 +193,10 @@ export interface ComponentConfigMeta {
|
|
|
194
193
|
/**
|
|
195
194
|
* A saved look, encapsulated: the colors and type plus a config for every
|
|
196
195
|
* component that sits off its default, all carried by value. Themes are the
|
|
197
|
-
* documents of the editor. Applying one opens it
|
|
198
|
-
*
|
|
199
|
-
* captures
|
|
196
|
+
* documents of the editor. Applying one opens it by clearing the reserved
|
|
197
|
+
* `_working` buffers and updating `themes/_active.json`; live reads fall through
|
|
198
|
+
* to its embedded copies. Saving captures live deltas back into it; Adopt
|
|
199
|
+
* publishes it, and only then is
|
|
200
200
|
* `tokens.generated.css` rebaked.
|
|
201
201
|
*/
|
|
202
202
|
export interface Theme {
|
|
@@ -33,7 +33,7 @@ A theme is a document, and the editor works the way any editor does.
|
|
|
33
33
|
`themes/_production.json`. Only **Adopt** changes it.
|
|
34
34
|
|
|
35
35
|
Absence is the answer for anything untouched: a buffer exists only where the
|
|
36
|
-
look
|
|
36
|
+
live look diverges from the active theme, so a newly opened theme has none.
|
|
37
37
|
|
|
38
38
|
## Saving
|
|
39
39
|
|
|
@@ -56,9 +56,10 @@ to it, and the editor never overwrites it, so start your own with **Save As**.
|
|
|
56
56
|
|
|
57
57
|
**Load** lists your saved themes and the seven example looks. Picking one shows
|
|
58
58
|
it on the page as a preview with nothing written to disk, so you can try each
|
|
59
|
-
look and compare. **Save** in that window opens the previewed theme:
|
|
60
|
-
|
|
61
|
-
the editor works on it from then on. **Cancel** returns
|
|
59
|
+
look and compare. **Save** in that window opens the previewed theme: the active
|
|
60
|
+
pointer changes, the buffers clear, components it does not carry fall through
|
|
61
|
+
to their defaults, and the editor works on it from then on. **Cancel** returns
|
|
62
|
+
you to where you were.
|
|
62
63
|
Trying a look never changes what your site ships.
|
|
63
64
|
|
|
64
65
|
**Colors and type only. Keep my shapes.** narrows the load to the palette and
|
|
@@ -34,12 +34,13 @@ deleting anything else never breaks it.
|
|
|
34
34
|
|
|
35
35
|
- **Editing** changes the page through CSS variables. The editor keeps your
|
|
36
36
|
edits in the browser as you work and writes them to the `_working.json`
|
|
37
|
-
buffers when you save a component
|
|
38
|
-
|
|
37
|
+
buffers when you save a component. A buffer exists only where the live layer
|
|
38
|
+
differs from the active theme's saved layer, so a fresh project has none.
|
|
39
39
|
- **Save** captures the buffers into the open theme's file. That file is the
|
|
40
|
-
durable copy of your look.
|
|
41
|
-
- **Load**
|
|
42
|
-
|
|
40
|
+
durable copy of your look; matching buffers are then removed.
|
|
41
|
+
- **Load** clears the buffers and points `themes/_active.json` at the theme you
|
|
42
|
+
picked. Live reads fall through to that file. Nothing else changes, so trying
|
|
43
|
+
looks is free and ordinary switching changes only the pointer.
|
|
43
44
|
- **Adopt** points `themes/_production.json` at the open theme, bakes it into
|
|
44
45
|
`tokens.generated.css`, and rewrites `fonts.css` to match. It is the only
|
|
45
46
|
action that changes what your site ships.
|
|
@@ -6,6 +6,6 @@ export const docContent: Record<string, string> = {
|
|
|
6
6
|
"creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
|
|
7
7
|
"editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has two views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Two curves shape how lightness and saturation fall off across the\n ramp. Drag the handles to bias it darker, lighter, or more saturated.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Overlays and gradients\n\n- **Overlays** are translucent tints layered over surfaces, like the subtle\n tint a card gets on hover. Set a colour and opacity per state.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
|
|
8
8
|
"getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your\n theme appears as JSON under `src/live-tokens/data/themes/`.\n5. Reload. The editor reopens on your theme, so the page returns as you left\n it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, click **Adopt** in the Theme panel. That saves the open theme and bakes\nit into `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Adopt is the only action that changes what your site\nships, so try any look you like first. The editor itself never reaches\nproduction.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
|
|
9
|
-
"themes-workflow": "# Themes\n\nSave your work, switch between looks, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole look:\ncolors, type, and a setting for every component you changed, in one file. It\ncarries the name the look ships under, whether production is running it, and\n**Adopt**. Two parts sit under it, each a read-out rather than a file to manage.\n\n- **Colors & Type** holds the design tokens. Components read those tokens to\n define their appearance. It names the two faces the page is showing.\n- **Components** counts how many components run something the theme does not\n carry, and opens the component editors.\n\nA theme holds its own copy of every part, so one theme can never break another.\n\n## How themes work\n\nA theme is a document, and the editor works the way any editor does.\n\n- **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries\n the whole look: the colors and type plus a setting for every component you\n changed.\n- **The open theme** is the one the editor is working on, named in\n `themes/_active.json`. One at a time.\n- **Your unsaved edits** are what the page shows right now. The editor keeps\n them in your browser as you work and writes them to a buffer, `_working.json`,\n one slot per part of the look. **Save** captures that buffer into the open\n theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`. Only **Adopt** changes it.\n\nAbsence is the answer for anything untouched: a buffer exists only where the\
|
|
10
|
-
"where-themes-live": "# Where themes live\n\nEverything the editor writes is plain JSON and CSS inside your project. There\nis no database and no hidden state: the files are the storage, and git is the\nhistory.\n\n## The data tree\n\n```\nsrc/live-tokens/data/\n themes/\n _active.json names the theme the editor has open\n _production.json names the theme your site ships\n default.json Motion Proto, the built-in look, rewritten at boot\n my-brand.json a saved theme: the whole look in one file\n colors-and-type/\n _working.json unsaved colors and type edits\n component-configs/\n button/\n default.json Button's shipped settings, derived at boot\n _working.json unsaved Button edits\n my-button.json a preset you saved from the Button editor\n tokens.generated.css the baked CSS your production build ships\nsrc/system/styles/\n tokens.css your token vocabulary, hand-authored, never written\n fonts.css font imports, rewritten when you Adopt\n```\n\nA saved theme carries the whole look by value: the colors and type plus a\nsetting for every component you changed. It depends on no other file, so\ndeleting anything else never breaks it.\n\n## What writes when\n\n- **Editing** changes the page through CSS variables. The editor keeps your\n edits in the browser as you work and writes them to the `_working.json`\n buffers when you save a component
|
|
9
|
+
"themes-workflow": "# Themes\n\nSave your work, switch between looks, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole look:\ncolors, type, and a setting for every component you changed, in one file. It\ncarries the name the look ships under, whether production is running it, and\n**Adopt**. Two parts sit under it, each a read-out rather than a file to manage.\n\n- **Colors & Type** holds the design tokens. Components read those tokens to\n define their appearance. It names the two faces the page is showing.\n- **Components** counts how many components run something the theme does not\n carry, and opens the component editors.\n\nA theme holds its own copy of every part, so one theme can never break another.\n\n## How themes work\n\nA theme is a document, and the editor works the way any editor does.\n\n- **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries\n the whole look: the colors and type plus a setting for every component you\n changed.\n- **The open theme** is the one the editor is working on, named in\n `themes/_active.json`. One at a time.\n- **Your unsaved edits** are what the page shows right now. The editor keeps\n them in your browser as you work and writes them to a buffer, `_working.json`,\n one slot per part of the look. **Save** captures that buffer into the open\n theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`. Only **Adopt** changes it.\n\nAbsence is the answer for anything untouched: a buffer exists only where the\nlive look diverges from the active theme, so a newly opened theme has none.\n\n## Saving\n\nIn the Theme panel:\n\n- **Save** captures the look on screen into the open theme. Your colors and type\n go in as part of it, so there is nothing to save first.\n- **Save As** names a new theme. Use it for your first save and for forking.\n\nComponent edits are the exception. Each component editor holds its own unsaved\nstate, which this panel cannot write, so save a component in its editor before\ncapturing it. The panel says how many are waiting.\n\nNames are tidied to lowercase with hyphens, so \"My Brand!\" becomes `my-brand`,\nand a leading underscore is dropped: those names are reserved for the buffer.\n**Motion Proto** is the built-in theme and is read-only. You can always return\nto it, and the editor never overwrites it, so start your own with **Save As**.\n\n## Switching\n\n**Load** lists your saved themes and the seven example looks. Picking one shows\nit on the page as a preview with nothing written to disk, so you can try each\nlook and compare. **Save** in that window opens the previewed theme: the active\npointer changes, the buffers clear, components it does not carry fall through\nto their defaults, and the editor works on it from then on. **Cancel** returns\nyou to where you were.\nTrying a look never changes what your site ships.\n\n**Colors and type only. Keep my shapes.** narrows the load to the palette and\nthe fonts: your component settings stay as they are and the theme you have open\nstays open. Saved colors and type files are listed there too, marked *colors &\ntype*, and picking one is always that narrower load.\n\n## Shipping\n\n**Adopt**, in the Theme panel, is the \"ship it\" step, and it ships the whole\nlook. It saves the open theme, then bakes that theme into\n`src/live-tokens/data/tokens.generated.css`, which your build bundles alongside\n`tokens.css`: the colors and type plus every component the theme carries. Fonts\nregenerate to match. The line under the theme name says whether production is\nrunning this theme.\n\nProduction is one saved theme, so nothing else publishes. Trying a look, moving\na token, saving a theme: all of it leaves the generated CSS alone until you\nAdopt. A component editor's Adopt runs the same whole-look step, because a\ncomponent never ships alone. Adopting while Motion Proto is open saves your look\nas a theme of your own first, since the built-in one is read-only.\n\nProduction builds (`npm run build`) ship only that plain CSS and your\ncomponents. No editor, no JSON loading, no runtime indirection.\n\n## Keeping your work safe\n\nEverything under `src/live-tokens/data/` is plain JSON, so commit it. Themes show\nup as readable diffs you can review per branch, and the buffer shows up as the\nwork you have not saved into a theme yet. Nothing is backed up anywhere else:\ngit is your safety net. To experiment freely, **Save As** a new name first, then\nedit.\n\n## Where to go next\n\n- **[Where themes live](where-themes-live.md)**: the files behind all of this,\n and what writes each one.\n- **[Creating components](creating-components.md)**: make your own components\n editable in the same editor.\n",
|
|
10
|
+
"where-themes-live": "# Where themes live\n\nEverything the editor writes is plain JSON and CSS inside your project. There\nis no database and no hidden state: the files are the storage, and git is the\nhistory.\n\n## The data tree\n\n```\nsrc/live-tokens/data/\n themes/\n _active.json names the theme the editor has open\n _production.json names the theme your site ships\n default.json Motion Proto, the built-in look, rewritten at boot\n my-brand.json a saved theme: the whole look in one file\n colors-and-type/\n _working.json unsaved colors and type edits\n component-configs/\n button/\n default.json Button's shipped settings, derived at boot\n _working.json unsaved Button edits\n my-button.json a preset you saved from the Button editor\n tokens.generated.css the baked CSS your production build ships\nsrc/system/styles/\n tokens.css your token vocabulary, hand-authored, never written\n fonts.css font imports, rewritten when you Adopt\n```\n\nA saved theme carries the whole look by value: the colors and type plus a\nsetting for every component you changed. It depends on no other file, so\ndeleting anything else never breaks it.\n\n## What writes when\n\n- **Editing** changes the page through CSS variables. The editor keeps your\n edits in the browser as you work and writes them to the `_working.json`\n buffers when you save a component. A buffer exists only where the live layer\n differs from the active theme's saved layer, so a fresh project has none.\n- **Save** captures the buffers into the open theme's file. That file is the\n durable copy of your look; matching buffers are then removed.\n- **Load** clears the buffers and points `themes/_active.json` at the theme you\n picked. Live reads fall through to that file. Nothing else changes, so trying\n looks is free and ordinary switching changes only the pointer.\n- **Adopt** points `themes/_production.json` at the open theme, bakes it into\n `tokens.generated.css`, and rewrites `fonts.css` to match. It is the only\n action that changes what your site ships.\n\nThe `default.json` files are the shipped baseline. The editor derives them at\nboot and refreshes them when the package updates; it never saves your work\nover them.\n\n## What to commit\n\nAll of it. The data tree is designed to live in git: themes diff readably, the\ntwo pointers say what is open and what ships, and a `_working.json` in a diff\nis exactly the work you have not yet saved into a theme. Nothing is backed up\nanywhere else.\n\n## Where to go next\n\n- **[Themes](themes-workflow.md)**: the workflow built on these files: saving,\n loading, and shipping.\n",
|
|
11
11
|
};
|
|
@@ -400,8 +400,8 @@
|
|
|
400
400
|
+ `saved settings were skipped:\n\n${result.skippedComponents.join(', ')}`,
|
|
401
401
|
);
|
|
402
402
|
}
|
|
403
|
-
// applyTheme opens the theme: it
|
|
404
|
-
// `themes/_active.json` at it. Reload to rehydrate the
|
|
403
|
+
// applyTheme opens the theme: it clears working deltas and points
|
|
404
|
+
// `themes/_active.json` at it. Reload to rehydrate through the resolver.
|
|
405
405
|
window.location.reload();
|
|
406
406
|
} catch (err) {
|
|
407
407
|
window.alert(`Failed to load theme: ${(err as Error).message}`);
|
|
@@ -511,8 +511,8 @@
|
|
|
511
511
|
async function handleDelete(row: LoadRow) {
|
|
512
512
|
if (row.isProtected) return;
|
|
513
513
|
if (row.kind === 'layer') return deleteColorsFile(row);
|
|
514
|
-
// Deleting the open theme is legal: the
|
|
515
|
-
//
|
|
514
|
+
// Deleting the open theme is legal: the server materialises only the
|
|
515
|
+
// deltas needed to keep the look on screen. Where open lands depends on
|
|
516
516
|
// something the client can't see — deleting a local copy that shadows a
|
|
517
517
|
// shipped theme restores the shipped one and keeps naming it, while
|
|
518
518
|
// deleting a local-only theme sends open back to Motion Proto.
|