@motion-proto/live-tokens 0.65.0 → 0.66.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 CHANGED
@@ -1,5 +1,73 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.66.0 — A theme's sketchstyle reaches the built site
4
+
5
+ ### Added
6
+
7
+ - **`seedSketchFromTheme` carries a theme's sketchstyle into a build.** A theme
8
+ saved from the Sketchstyle view carries its dials, and 0.63.0 said in as many
9
+ words that a built site ships no sketch. That left a theme half applied: the
10
+ page in dev drew with the look the theme records, the page a visitor gets drew
11
+ with whatever shipped preset the bundle happened to hold. Three links dropped
12
+ it, and only one of them was a decision. `initializeTheme` runs behind
13
+ `import.meta.env.DEV`, so nothing in a build ever read the field; the entry
14
+ point exported no way to apply a look it had not shipped; and there was
15
+ nothing for the field to be baked into, since the layer is an SVG filter bank
16
+ rather than a set of custom properties.
17
+
18
+ The new export is the whole route. Hand it the theme's `sketchStyle` field
19
+ before mounting and the built page draws with it:
20
+
21
+ ```ts
22
+ import { seedSketchFromTheme } from '@motion-proto/live-tokens/sketch';
23
+
24
+ seedSketchFromTheme(theme.sketchStyle);
25
+ await bootLiveTokens(App, '#app');
26
+ ```
27
+
28
+ It takes the field raw and hydrates it, because a built site reads its theme
29
+ JSON with no dev server to run `normalizeTheme` over it first. Absent, `null`,
30
+ and anything that is not an object all mean no sketch, which is what absence
31
+ has always meant.
32
+
33
+ It is the rule boot already followed, not a second one: `initializeTheme` now
34
+ calls it too, so one piece of code decides what a theme's sketchstyle means at
35
+ boot in dev and in production. A visitor who has recorded a pick keeps it,
36
+ None included. The theme seeds a browser that has decided nothing and never
37
+ overwrites one that has, so calling it on every boot is safe.
38
+
39
+ Adopt still bakes nothing, and `tokens.generated.css` still holds token values
40
+ only. That half of 0.63.0's note stands; what it said about a built site
41
+ shipping no sketch does not.
42
+
43
+ - **`themeSketchLook` is the theme's own look, as a picker row.** A seeded look
44
+ that no shipped sketchstyle names read as `adjusted` through `sketchPick`, so
45
+ a picker could only label a look it had just booted into "Adjusted", and a
46
+ visitor who moved off it had no way back. The new store carries the same
47
+ `id`/`label`/`blurb` shape a shipped look has, `setSketch` takes its id, and
48
+ `sketchPick` reports it as the look it is. It is null when the theme carries
49
+ no sketchstyle, and null when what it carries is one of the shipped looks,
50
+ since that look's own row already names it.
51
+
52
+ - **`SketchStyle` is exported from `@motion-proto/live-tokens/sketch`**, so a
53
+ site can type the field it pulled out of its own theme JSON.
54
+
55
+ ## 0.65.1 — applyFontStacks returns what it wrote
56
+
57
+ ### Changed
58
+
59
+ - **`applyFontStacks` returns the variables it wrote.** It writes the `--font-*`
60
+ stacks from a list it kept to itself, so a consumer tracking what it had
61
+ applied — to tear those vars down when switching looks — had to hand-maintain
62
+ a copy of that list. A copy falls behind silently the moment a stack is added
63
+ here, and the missed variable stays stuck at the outgoing look's value:
64
+ `--font-editorial` did exactly that to a site that had listed the original
65
+ four. The return value is additive, so existing calls keep working.
66
+
67
+ ```ts
68
+ applied = [...applied, ...applyFontStacks(theme.fontStacks, theme.fontSources)];
69
+ ```
70
+
3
71
  ## 0.65.0 — A link the router cannot serve is the browser's
4
72
 
5
73
  ### Fixed
@@ -13,7 +13,7 @@ import {
13
13
  nextAvailableName,
14
14
  normalizeTheme,
15
15
  versionedFileResourceServer
16
- } from "./chunk-NDJJORKJ.js";
16
+ } from "./chunk-NRONZ3T5.js";
17
17
  import {
18
18
  CURRENT_COMPONENT_SCHEMA_VERSION
19
19
  } from "./chunk-NE6N66EE.js";
@@ -7,7 +7,7 @@ import {
7
7
  normalizeTheme,
8
8
  planLegacyRenames,
9
9
  versionedFileResourceServer
10
- } from "../chunk-NDJJORKJ.js";
10
+ } from "../chunk-NRONZ3T5.js";
11
11
  import {
12
12
  CURRENT_COMPONENT_SCHEMA_VERSION
13
13
  } from "../chunk-NE6N66EE.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -129,12 +129,27 @@ export function resolveFontStackValues(
129
129
  * Compose each stack into its resolved "family1, family2, ..." string and
130
130
  * write it to the matching --font-* variable on :root (and parent :root when
131
131
  * in an iframe) via the same pipeline used for color variables.
132
+ *
133
+ * Returns the variables it set, so a caller tracking what it has applied — to
134
+ * tear those vars down when switching looks — can record them without keeping
135
+ * its own copy of the stack list. A hand-maintained copy silently falls behind
136
+ * whenever a stack is added here, leaving the missed variable stuck at the
137
+ * outgoing look's value; `--font-editorial` did exactly that.
132
138
  */
133
- export function applyFontStacks(stacks: FontStack[], sources: FontSource[]): void {
139
+ export function applyFontStacks(
140
+ stacks: FontStack[],
141
+ sources: FontSource[],
142
+ ): FontStackVariable[] {
134
143
  const resolved = resolveFontStackValues(stacks, sources);
144
+ const applied: FontStackVariable[] = [];
135
145
  for (const name of FONT_STACK_VARIABLES) {
136
146
  const value = resolved[name];
137
- if (value) setCssVar(name, value);
138
- else removeCssVar(name);
147
+ if (value) {
148
+ setCssVar(name, value);
149
+ applied.push(name);
150
+ } else {
151
+ removeCssVar(name);
152
+ }
139
153
  }
154
+ return applied;
140
155
  }
@@ -1,6 +1,14 @@
1
- import { derived, type Readable } from 'svelte/store';
2
- import { SKETCH_STYLES } from './sketchStyles';
3
- import { selectSketchStyle, setSketchEnabled, sketchEnabled, sketchStyleName } from './sketchStore';
1
+ import { derived, get, type Readable } from 'svelte/store';
2
+ import { SKETCH_STYLES, THEME_SKETCH_ID } from './sketchStyles';
3
+ import {
4
+ sameLook,
5
+ selectSketchStyle,
6
+ selectThemeSketchStyle,
7
+ setSketchEnabled,
8
+ sketchEnabled,
9
+ sketchStyleName,
10
+ themeSketchStyle,
11
+ } from './sketchStore';
4
12
 
5
13
  export interface SketchLook {
6
14
  /** What `setSketch` takes. */
@@ -10,11 +18,30 @@ export interface SketchLook {
10
18
  }
11
19
 
12
20
  /** The shipped sketchstyles. A picker adds its own "None" row: off is a state
13
- of the effect, not one of the looks. */
21
+ of the effect, not one of the looks. A theme's own look is not here either,
22
+ since it is not shipped; `themeSketchLook` carries that one row. */
14
23
  export const SKETCH_LOOKS: readonly SketchLook[] = Object.entries(SKETCH_STYLES).map(
15
24
  ([id, style]) => ({ id, label: style.label, blurb: style.blurb }),
16
25
  );
17
26
 
27
+ /**
28
+ * The look the open theme carries, as one more row for a picker: same shape as
29
+ * a shipped look, and `setSketch` takes its id like any other. Null when the
30
+ * theme carries no sketchstyle, and null when what it carries IS one of the
31
+ * shipped looks, since that look's own row already names it.
32
+ *
33
+ * Without this row the theme's look is a one-way door: a visitor lands on it,
34
+ * picks Pencil, and nothing can take them back. It is also the only thing that
35
+ * can name that look, which no shipped label can do honestly. A theme tuned off
36
+ * `marker` still carries the label "Marker", so a row built from the style's
37
+ * own label would sit beside the shipped Marker claiming to be it.
38
+ */
39
+ export const themeSketchLook: Readable<SketchLook | null> = derived(themeSketchStyle, (style) => {
40
+ if (!style) return null;
41
+ if (Object.values(SKETCH_STYLES).some((shipped) => sameLook(shipped, style))) return null;
42
+ return { id: THEME_SKETCH_ID, label: 'Theme', blurb: 'The look this theme carries.' };
43
+ });
44
+
18
45
  /**
19
46
  * What the page is drawing with. Three states, not two: the effect can be on
20
47
  * under a look no shipped sketchstyle names — one saved to a file, or one a
@@ -31,10 +58,11 @@ export type SketchPick =
31
58
  | { state: 'adjusted' };
32
59
 
33
60
  export const sketchPick: Readable<SketchPick> = derived(
34
- [sketchEnabled, sketchStyleName],
35
- ([on, name]): SketchPick => {
61
+ [sketchEnabled, sketchStyleName, themeSketchLook],
62
+ ([on, name, themeLook]): SketchPick => {
36
63
  if (!on) return { state: 'off' };
37
- const look = SKETCH_LOOKS.find((l) => l.id === name);
64
+ const look =
65
+ SKETCH_LOOKS.find((l) => l.id === name) ?? (themeLook && themeLook.id === name ? themeLook : undefined);
38
66
  return look ? { state: 'look', look } : { state: 'adjusted' };
39
67
  },
40
68
  );
@@ -52,8 +80,16 @@ export function setSketch(id: string | null): void {
52
80
  setSketchEnabled(false);
53
81
  return;
54
82
  }
83
+ if (id === THEME_SKETCH_ID) {
84
+ if (!get(themeSketchStyle)) {
85
+ throw new Error('No theme sketchstyle to draw with. `themeSketchLook` is null unless a theme carries one.');
86
+ }
87
+ selectThemeSketchStyle();
88
+ setSketchEnabled(true);
89
+ return;
90
+ }
55
91
  if (!(id in SKETCH_STYLES)) {
56
- throw new Error(`Unknown sketchstyle "${id}". Ids come from SKETCH_LOOKS.`);
92
+ throw new Error(`Unknown sketchstyle "${id}". Ids come from SKETCH_LOOKS and themeSketchLook.`);
57
93
  }
58
94
  selectSketchStyle(id);
59
95
  setSketchEnabled(true);
@@ -68,3 +104,28 @@ export function setSketch(id: string | null): void {
68
104
  * key directly is not an option worth offering — it is ours to rename.
69
105
  */
70
106
  export { hasPersistedSketchState } from './sketchStore';
107
+
108
+ /**
109
+ * Draw the page with the sketchstyle a theme carries, unless this browser has
110
+ * already decided for itself.
111
+ *
112
+ * The route from a saved theme to a built page. Hand it the theme's
113
+ * `sketchStyle` field, raw: a built site has no theme API, so it reads its own
114
+ * theme JSON and this hydrates what it finds. Absent, `null`, or anything that
115
+ * is not an object all mean the same thing, which is no sketch.
116
+ *
117
+ * Call it before mounting, the way dev boot does (`bootstrap.ts` awaits
118
+ * `initializeTheme` first), so the look is up on the first frame rather than
119
+ * arriving over a crisp page.
120
+ *
121
+ * A visitor who has recorded a pick of their own keeps it, None included: this
122
+ * seeds an undecided browser and never overwrites a decided one, so it is safe
123
+ * to call on every boot. `themeSketchLook` is populated either way, so a picker
124
+ * can offer the theme's look as a row whether or not this painted it.
125
+ */
126
+ export { seedSketchFromTheme } from './sketchStore';
127
+
128
+ /** The dial set a theme's `sketchStyle` field holds, for a consumer typing the
129
+ value it pulled out of its own theme JSON. `seedSketchFromTheme` takes it
130
+ raw, so nothing has to be cast to hand it over. */
131
+ export type { SketchStyle } from './sketchStyles';
@@ -2,6 +2,7 @@ import { derived, get, writable } from 'svelte/store';
2
2
  import {
3
3
  SKETCH_STYLES,
4
4
  DEFAULT_SKETCH_STYLE,
5
+ THEME_SKETCH_ID,
5
6
  hydrateSketchStyle,
6
7
  type SketchStyle,
7
8
  } from './sketchStyles';
@@ -108,7 +109,10 @@ export const USER_STYLE_PREFIX = 'user:';
108
109
  function readStyleName(): string {
109
110
  try {
110
111
  const name = localStorage.getItem(STYLE_NAME_KEY);
111
- if (name === '' || (name && (name in SKETCH_STYLES || name.startsWith(USER_STYLE_PREFIX)))) {
112
+ if (
113
+ name === '' ||
114
+ (name && (name in SKETCH_STYLES || name === THEME_SKETCH_ID || name.startsWith(USER_STYLE_PREFIX)))
115
+ ) {
112
116
  return name;
113
117
  }
114
118
  } catch {
@@ -122,8 +126,9 @@ function readStyleName(): string {
122
126
  export const sketchEnabled = writable<boolean>(readEnabled());
123
127
  export const sketchSettings = writable<SketchStyle>(readSettings());
124
128
  /** The sketchstyle the dials started from. It survives dial moves, so the grid
125
- keeps showing what the current look is closest to; empty only when nothing
126
- was picked, or the picked file was deleted. */
129
+ keeps showing what the current look is closest to. A shipped id, a `user:`
130
+ file, or `THEME_SKETCH_ID` for the look the open theme carries; empty only
131
+ when nothing was picked, or the picked file was deleted. */
127
132
  export const sketchStyleName = writable<string>(readStyleName());
128
133
 
129
134
  /** The settings as the selected sketchstyle defined them. Kept beside the live
@@ -134,7 +139,7 @@ export const sketchBaseline = writable<SketchStyle | null>(readBaseline());
134
139
 
135
140
  /** Dial-set fields only. `label` and `blurb` name the sketchstyle rather than
136
141
  describe the look, and no dial writes them. */
137
- function sameLook(a: SketchStyle, b: SketchStyle): boolean {
142
+ export function sameLook(a: SketchStyle, b: SketchStyle): boolean {
138
143
  return (Object.keys(a) as (keyof SketchStyle)[])
139
144
  .filter((k) => k !== 'label' && k !== 'blurb')
140
145
  .every((k) => a[k] === b[k]);
@@ -184,10 +189,53 @@ export function openThemeSketchStyle(sketchStyle: SketchStyle | undefined): void
184
189
  const matched = (Object.keys(SKETCH_STYLES) as string[]).find((name) => sameLook(SKETCH_STYLES[name], sketchStyle));
185
190
  sketchSettings.set({ ...sketchStyle });
186
191
  sketchBaseline.set({ ...sketchStyle });
187
- sketchStyleName.set(matched ?? '');
192
+ sketchStyleName.set(matched ?? THEME_SKETCH_ID);
188
193
  sketchEnabled.set(true);
189
194
  }
190
195
 
196
+ /**
197
+ * Take the sketchstyle a theme carries as this browser's own, unless this
198
+ * browser has already decided for itself.
199
+ *
200
+ * The rule boot has always followed in dev, and the only one a built site has:
201
+ * a visitor who picked a look, or picked None, keeps it, and `themeSketchStyle`
202
+ * still learns what the theme holds so the panel can call the difference
203
+ * unsaved. Both branches set it, so a picker can offer the theme's look as a
204
+ * row either way.
205
+ *
206
+ * Takes the raw field rather than a `SketchStyle`, and hydrates it here: a
207
+ * built site reads its theme JSON straight off disk with no dev server to run
208
+ * `normalizeTheme` over it first, so this is the only place a look stored under
209
+ * a retired dial name gets carried forward. Anything that is not an object is
210
+ * the absent case, which is off (invariant 3).
211
+ */
212
+ export function seedSketchFromTheme(sketchStyle: unknown): void {
213
+ const style =
214
+ typeof sketchStyle === 'object' && sketchStyle !== null && !Array.isArray(sketchStyle)
215
+ ? hydrateSketchStyle(sketchStyle)
216
+ : undefined;
217
+ if (hasPersistedSketchState()) {
218
+ themeSketchStyle.set(style);
219
+ return;
220
+ }
221
+ openThemeSketchStyle(style);
222
+ }
223
+
224
+ /** Go back to the look the theme carries, after picking something else. The
225
+ theme's is the one look a picker can offer that this module did not ship, so
226
+ it needs a door of its own beside `selectSketchStyle`; `setSketch` gives the
227
+ two the same face. Silent when the theme carries none, the way
228
+ `selectSketchStyle` is for a name it does not know. */
229
+ export function selectThemeSketchStyle(): void {
230
+ const style = get(themeSketchStyle);
231
+ if (!style) return;
232
+ markSketchTouched();
233
+ if (get(sketchEnabled)) liveMovedSinceBake.set(true);
234
+ sketchStyleName.set(THEME_SKETCH_ID);
235
+ sketchBaseline.set({ ...style });
236
+ sketchSettings.set({ ...style });
237
+ }
238
+
191
239
  /** The live sketch differs from what the open theme carries. Presence is
192
240
  half the comparison: on with dials the theme does not hold, or off while
193
241
  the theme holds a layer, are both off the theme. */
@@ -216,6 +216,12 @@ export const SKETCH_STYLES: Record<string, SketchStyle> = {
216
216
 
217
217
  export const DEFAULT_SKETCH_STYLE = 'marker';
218
218
 
219
+ /** The id of the look a theme carries, in the same id namespace as the shipped
220
+ sketchstyles so one picker row and one `setSketch` call cover both. Never a
221
+ key of `SKETCH_STYLES`: a shipped style claiming it would shadow the theme's
222
+ own look in every picker. `index.test.ts` pins that. */
223
+ export const THEME_SKETCH_ID = 'theme';
224
+
219
225
  /** Reconciled against a full sketchstyle in both directions: a value stored before a
220
226
  control existed picks up the default, and a value stored for a control since
221
227
  retired is dropped. Without the drop, a stale key survives every spread and
@@ -5,7 +5,7 @@ import { loadFromFile, seedComponentsFromApi } from '../store/editorStore';
5
5
  import { getActiveComponentConfig, type ComponentSummary } from '../components/componentConfigService';
6
6
  import { safeFetch } from '../storage/storage';
7
7
  import { API_BASE } from '../storage/apiBase';
8
- import { hasPersistedSketchState, openThemeSketchStyle, themeSketchStyle } from '../sketch/sketchStore';
8
+ import { seedSketchFromTheme } from '../sketch/sketchStore';
9
9
 
10
10
  interface ListComponentsDto {
11
11
  components: ComponentSummary[];
@@ -67,16 +67,7 @@ export async function initializeTheme(): Promise<void> {
67
67
  // A failed fetch is not "the theme carries no sketchstyle": treating null as
68
68
  // absent would tell the panel the look is off the theme, or hand a fresh
69
69
  // browser a blank buffer, over a fetch that will likely succeed next time.
70
- if (active) {
71
- if (hasPersistedSketchState()) {
72
- // The buffer already painted on the first frame; boot only learns what
73
- // the theme holds so unsaved dial work reads as unsaved rather than
74
- // getting silently overwritten (that overwrite is what Apply is for).
75
- themeSketchStyle.set(active.sketchStyle);
76
- } else {
77
- // Nothing was ever recorded in this browser: the theme's value becomes
78
- // the live value, the same reconciliation opening a theme performs.
79
- openThemeSketchStyle(active.sketchStyle);
80
- }
81
- }
70
+ // The same call a built site makes (`@motion-proto/live-tokens/sketch`), so
71
+ // one rule decides what a theme's sketchstyle means at boot in both.
72
+ if (active) seedSketchFromTheme(active.sketchStyle);
82
73
  }
@@ -6,7 +6,7 @@ they are drawn with.
6
6
 
7
7
  It is an effect layer, not a set of token values. It never touches a token
8
8
  itself, so turning it off returns every component to exactly what its tokens
9
- already say. A production build never carries the drawing.
9
+ already say.
10
10
 
11
11
  Open the **Sketchstyle** view in the editor and switch **Sketch mode** on. The effect
12
12
  applies to the page behind the editor as well as to the preview, so what you see
@@ -86,9 +86,36 @@ writes a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a look you
86
86
  can pick from any theme. It never touches the open theme, and it never marks
87
87
  the look off the theme.
88
88
 
89
- Sketch mode is a tool for looking at the page, not a layer the page can ship.
90
- The theme records the dials, but nothing bakes them: `tokens.generated.css`
91
- never sees them, and a production build has no sketch layer in it at all.
89
+ ## Shipping the layer
90
+
91
+ The dev server reads the open theme and paints whatever it carries. A built site
92
+ has no server to ask, so it hands the field over itself:
93
+
94
+ ```ts
95
+ import { seedSketchFromTheme } from '@motion-proto/live-tokens/sketch';
96
+ import theme from './live-tokens/data/themes/sketchy.json';
97
+
98
+ seedSketchFromTheme(theme.sketchStyle);
99
+ await bootLiveTokens(App, '#app');
100
+ ```
101
+
102
+ Call it before mounting, so the look is up on the first frame. Pass the field
103
+ raw. A theme written against older dial names is carried forward on the way in,
104
+ the same reconciliation the dev server runs on every theme it reads.
105
+
106
+ Nothing is baked. `tokens.generated.css` still holds token values only, and the
107
+ layer stays JavaScript the page runs, because it builds an SVG filter bank
108
+ rather than a set of custom properties.
109
+
110
+ A visitor who has picked a look of their own keeps it, None included. The theme
111
+ seeds a browser that has decided nothing and never overwrites one that has, so
112
+ calling this on every boot is safe.
113
+
114
+ If your site offers a sketch picker, `themeSketchLook` is the theme's own look
115
+ as one more row. It is null when the theme carries none, and null when what it
116
+ carries is one of the shipped seven, since that row already names it. Give the
117
+ row `setSketch(look.id)` like any other, and a visitor who wanders off the
118
+ theme's look can come back to it.
92
119
 
93
120
  ## Drawing your own elements
94
121
 
@@ -7,7 +7,7 @@ export const docContent: Record<string, string> = {
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 four views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\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.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\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
9
  "light-and-dark": "# Light and dark\n\nSome things on a page cannot be written as a token. A wordmark drawn in white\ndisappears on a pale theme. Ink that multiplies onto paper vanishes on a dark\none. A photograph behind a headline is dark no matter what the palette says.\n\nEach of those needs the same fact first: which way does the surface behind this\nthing lean? One attribute carries it.\n\n## The attribute\n\n`data-backdrop` is either `light` or `dark`, and it does two things at once: it\nselects, so a rule can key on it, and it sets `color-scheme`, so every\n`light-dark()` under it resolves the half that reads.\n\n```css\n.title {\n color: light-dark(var(--color-black), var(--color-white));\n}\n```\n\nThat line is right on both sides of the theme, and it is right inside a dark\nband on a pale page, because the nearest `color-scheme` wins.\n\n## Stating it\n\nPut it in the markup when the surface knows its own tone — a hero over a\nphotograph, a plate that stays pale in every theme:\n\n```svelte\n<div class=\"hero-panel\" data-backdrop=\"dark\">\n```\n\nA stated tone beats any measurement, and it inherits, so everything inside the\npanel resolves against it.\n\n## Measuring it\n\nWhere the tone is a property of the theme rather than of the markup, let it be\nmeasured:\n\n```svelte\n<script>\n import { backdrop } from '@motion-proto/live-tokens/backdrop';\n</script>\n\n<section use:backdrop>\n```\n\nThe action reads whatever actually paints behind the element — the nearest\nancestor with an opaque fill, averaged across its gradient stops, falling back\nto the theme's `--page-bg` — and stamps the answer. It re-reads when the theme\nchanges, which the editor does by rewriting custom properties with no reload,\nso the stamp follows a live edit.\n\nThe page itself is stamped for you: the build bakes the production theme's\npolarity into `tokens.generated.css`, so the first paint is already right, and\n`syncDocumentBackdrop()` keeps `<html>` current as themes switch.\n\n```ts\nimport { syncDocumentBackdrop } from '@motion-proto/live-tokens/backdrop';\n\nsyncDocumentBackdrop();\n```\n\n## Reading it from JavaScript\n\nAnything that paints outside CSS — a canvas, a WebGL uniform, an `<img>` that\ncomes in two versions — asks the same question through the same module:\n\n```ts\nimport { isLightBackdrop, watchBackdrop, cssColorToHex } from '@motion-proto/live-tokens/backdrop';\n\nconst stop = watchBackdrop(logoEl, {\n stamp: false,\n onChange: (polarity) => (src = polarity === 'light' ? darkMark : lightMark),\n});\n```\n\n`isLightBackdrop(el)` answers once. `watchBackdrop` keeps answering and returns\na stop function. `cssColorToHex` resolves any CSS colour — including the\n`oklch()` a token holds — to a hex a non-CSS consumer can take.\n\n## What it does not do\n\nPolarity is a property of a surface, not of a component, so nothing is stamped\nfor you below `<html>`: a section that needs an answer either states one or asks\nfor one. And a measurement reads the paint at the moment it runs — an element\nthat scrolls from a pale band onto a dark one keeps the answer it was given.\nState the tone on each band instead.\n",
10
- "sketch-mode": "# Sketch mode\n\nSketch mode redraws your whole page as if it had been drawn by hand. Every\ncomponent keeps its own colours, spacing and corners; what changes is the line\nthey are drawn with.\n\nIt is an effect layer, not a set of token values. It never touches a token\nitself, so turning it off returns every component to exactly what its tokens\nalready say. A production build never carries the drawing.\n\nOpen the **Sketchstyle** view in the editor and switch **Sketch mode** on. The effect\napplies to the page behind the editor as well as to the preview, so what you see\nin context is what it does.\n\n## What it draws\n\nEach component's fill and outline are repainted from the tokens that component\nalready owns. The real background and border are hidden behind them, then both\nare pushed around one shared field of noise. Because every component samples the\nsame field, the whole page reads as one drawing rather than as a set of\nseparately wobbled boxes.\n\n## The sketchstyles\n\nSeven looks ship with the package, and each is a complete set of dials rather\nthan just a name:\n\n- **Pencil.** Two graphite passes on their own seeds, so the outline disagrees\n with itself the way a hand coming back round does.\n- **Marker.** A broad translucent nib gone round twice on the same line, so the\n overlap darkens and the ink pools where it slows.\n- **Whiteboard.** The fattest nib on glass, with a mask that streaks the fill\n like a half-wiped board.\n- **Hatched.** An etching. The fill is angled shading and the outline a single\n hard-edged scratch.\n- **Dashed.** A drafting outline: one slow drift along the ruler, broken into\n strokes. The clean pole.\n- **Napkin.** Ballpoint in a hurry. Everything loose at once.\n- **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.\n\nPick one, then move whatever you like. **Save as sketchstyle…** keeps your\ndials under a name of your own, alongside the shipped seven, as a file under\n`src/live-tokens/data/sketch-styles/`. That is a different gesture from\nsaving a theme; see \"Where the settings live\" below.\n\n## The dials\n\n- **Border.** How far the outline travels and how long its wave is, then its\n width, ink, pressure and pooling. A second pass either copies the first line a\n few pixels off or runs it through the pen again on its own seed.\n- **Fill.** Solid or hatched, how far the fill's edge travels, and how far each\n instance is offset, rotated and scaled from its neighbours. **Ink coverage**\n thins the fill with a field of blotches: set their size and how many levels of\n detail, then work the field as a levels control. The field always runs black\n to white whatever the noise underneath. Steps flattens it into tones, and\n Output squeezes the whole of it into the range the ink covers, from how pale\n it gets at its thinnest to how dense at its fullest. Menus and tooltips are\n drawn solid whatever the coverage dials say: they float over the page, and a\n fill worn through in patches lets the page show through them.\n- **Shape.** **Corner spread** rounds each corner by its own share of the dial,\n so no two match. **Corner travel** leans the drawn box into a quadrilateral\n with no two sides parallel. This is the dial that stops a component reading as\n a rectangle.\n- **Icons and SVG.** Glyph travel and wavelength on their own scale. A glyph is\n all curves already, so it needs more travel than a card's long straight edge\n before the wobble reads at all.\n- **Noise.** The shared field itself: its wavelength, how many layers of detail\n sit on it, and the shape of its wave. A square wave sends nearly every edge to\n full travel, which is what makes the effect stronger rather than bigger.\n\n## Where the settings live\n\nThe sketch layer is part of the theme, the same way colors and type are.\n**Save** in the Theme panel folds your dials into the open theme; **Load**\napplies whatever a theme carries, and turns the effect off for a theme that\ncarries none.\n\nUntil you save, the dials sit in your browser only. The Theme panel calls\nthat state off the theme, the same word it uses for an unsaved component\nchange. The built-in **Motion Proto** theme is read-only, so Save\nis disabled there; use **Save As** to fold the dials into a theme of your\nown.\n\n**Save as sketchstyle…** in the **Sketchstyle** view is a different gesture. It\nwrites a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a look you\ncan pick from any theme. It never touches the open theme, and it never marks\nthe look off the theme.\n\nSketch mode is a tool for looking at the page, not a layer the page can ship.\nThe theme records the dials, but nothing bakes them: `tokens.generated.css`\nnever sees them, and a production build has no sketch layer in it at all.\n\n## Drawing your own elements\n\nThe layer draws a fixed set of parts: the shipped components, and four classes\nit reserves for you. Nothing else is touched, so a page element or a\nconsumer-authored component is left crisp until it carries one of them.\n\n| Class | For |\n|---------------------|-----------------------------------------------------------|\n| `sketch-surface` | A box. The default treatment. |\n| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |\n| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |\n| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |\n\nPick by size, not by kind: a card and a modal both take `sketch-container`, a\nbadge and a pill both take `sketch-chip`.\n\nThe class opts the element in; it names no colours, so the element states its\nown. `--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color`,\n`--sketch-radius` and `--sketch-shadow` name the fill, the outline, the hatching\nink, the corners and the shadow for one element and everything inside it. The\nlayer blanks the real background and border, so an element whose fill matters\nunder Sketch mode has to name it here as well as paint it.\n\nThe layer also paints on the element's `::before` and `::after`, forces its\n`overflow` visible, and gives it a stacking context of its own. Keep the class\noff anything that owns a pseudo-element, clips its content, or is positioned\nabsolutely, and put it on a wrapper instead.\n\n```css\n.my-callout {\n background: var(--surface-brand-lowest);\n border: var(--border-width-1) solid var(--border-brand);\n border-radius: var(--radius-xl);\n\n --sketch-fill: var(--surface-brand-lowest);\n --sketch-stroke: var(--border-brand);\n --sketch-radius: var(--radius-xl);\n}\n```\n\nA gradient is a valid fill: the shorthand's last layer takes a colour or an\nimage, so `--sketch-fill` accepts either. States work the same way, since\nnothing is competing with you for the value:\n\n```css\n.my-callout:hover { --sketch-stroke: var(--border-brand-strong); }\n```\n\n## Images inside a drawn part\n\nA drawn part's `overflow` is forced visible, because the fill and outline are\npainted on pseudo-elements that travel past the box and would otherwise be cut\noff at its edge. A background that bleeds is the effect working. An image that\nbleeds is not: it keeps its square corners while the card around it turns.\n\nMedia that runs to a part's edge therefore has to carry that part's corners\nitself. `--sketch-radius` is the radius the layer drew, and it inherits, so a\nchild can read it and fall back to its own value when Sketch mode is off:\n\n```css\n.cover {\n overflow: hidden;\n border-top-left-radius: var(--sketch-radius, var(--card-default-radius));\n border-top-right-radius: var(--sketch-radius, var(--card-default-radius));\n}\n```\n\nCorner spread is per-corner and per-instance, so at high spread the crop is the\nmean rather than an exact trace of the drawn edge.\n\nA rule made from a `border` is not a box and cannot be displaced. Make it an\nelement, give it `sketch-rule`, and name its ink:\n\n```html\n<div class=\"rule sketch-rule\"></div>\n```\n```css\n.rule {\n height: var(--border-width-2);\n background: var(--border-brand);\n --sketch-fill: var(--border-brand);\n}\n```\n\nIcons and inline SVG take the wobble directly, since a glyph has no box to\nredraw. Body type is left alone: an icon is a shape and survives a wobble, a\nparagraph is not.\n\n`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It\ninherits, so one declaration covers everything under it, and it takes the ink\nmask off as well as the wobble:\n\n```css\n/* Crisp. Chrome, a logo, anything that has to stay exact. */\n.app-bar { --sketch-icon-off: none; }\n\n/* Drawn back rather than off, at a third of the travel. Small artwork, and\n type set as an SVG, which the layer reads as one large glyph. */\n.wordmark { --sketch-icon-off: var(--sketch-icon-soft); }\n```\n\nThe **Blotch size** dial under Icons and SVG is a share of the glyph rather than\na px size, because no px size is right for both a 16px icon and a page-wide\ndrawing. At 100% every glyph gets one period of the field across it whatever its\nsize. Below that the field repeats inside the glyph and the blotches get finer.\nAbove it a glyph reads part of one blotch, so the mask thins the whole glyph\nunevenly instead of breaking it up. The fill's blotches stay in px, since a\ncomponent does have a size to state one against.\n",
10
+ "sketch-mode": "# Sketch mode\n\nSketch mode redraws your whole page as if it had been drawn by hand. Every\ncomponent keeps its own colours, spacing and corners; what changes is the line\nthey are drawn with.\n\nIt is an effect layer, not a set of token values. It never touches a token\nitself, so turning it off returns every component to exactly what its tokens\nalready say.\n\nOpen the **Sketchstyle** view in the editor and switch **Sketch mode** on. The effect\napplies to the page behind the editor as well as to the preview, so what you see\nin context is what it does.\n\n## What it draws\n\nEach component's fill and outline are repainted from the tokens that component\nalready owns. The real background and border are hidden behind them, then both\nare pushed around one shared field of noise. Because every component samples the\nsame field, the whole page reads as one drawing rather than as a set of\nseparately wobbled boxes.\n\n## The sketchstyles\n\nSeven looks ship with the package, and each is a complete set of dials rather\nthan just a name:\n\n- **Pencil.** Two graphite passes on their own seeds, so the outline disagrees\n with itself the way a hand coming back round does.\n- **Marker.** A broad translucent nib gone round twice on the same line, so the\n overlap darkens and the ink pools where it slows.\n- **Whiteboard.** The fattest nib on glass, with a mask that streaks the fill\n like a half-wiped board.\n- **Hatched.** An etching. The fill is angled shading and the outline a single\n hard-edged scratch.\n- **Dashed.** A drafting outline: one slow drift along the ruler, broken into\n strokes. The clean pole.\n- **Napkin.** Ballpoint in a hurry. Everything loose at once.\n- **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.\n\nPick one, then move whatever you like. **Save as sketchstyle…** keeps your\ndials under a name of your own, alongside the shipped seven, as a file under\n`src/live-tokens/data/sketch-styles/`. That is a different gesture from\nsaving a theme; see \"Where the settings live\" below.\n\n## The dials\n\n- **Border.** How far the outline travels and how long its wave is, then its\n width, ink, pressure and pooling. A second pass either copies the first line a\n few pixels off or runs it through the pen again on its own seed.\n- **Fill.** Solid or hatched, how far the fill's edge travels, and how far each\n instance is offset, rotated and scaled from its neighbours. **Ink coverage**\n thins the fill with a field of blotches: set their size and how many levels of\n detail, then work the field as a levels control. The field always runs black\n to white whatever the noise underneath. Steps flattens it into tones, and\n Output squeezes the whole of it into the range the ink covers, from how pale\n it gets at its thinnest to how dense at its fullest. Menus and tooltips are\n drawn solid whatever the coverage dials say: they float over the page, and a\n fill worn through in patches lets the page show through them.\n- **Shape.** **Corner spread** rounds each corner by its own share of the dial,\n so no two match. **Corner travel** leans the drawn box into a quadrilateral\n with no two sides parallel. This is the dial that stops a component reading as\n a rectangle.\n- **Icons and SVG.** Glyph travel and wavelength on their own scale. A glyph is\n all curves already, so it needs more travel than a card's long straight edge\n before the wobble reads at all.\n- **Noise.** The shared field itself: its wavelength, how many layers of detail\n sit on it, and the shape of its wave. A square wave sends nearly every edge to\n full travel, which is what makes the effect stronger rather than bigger.\n\n## Where the settings live\n\nThe sketch layer is part of the theme, the same way colors and type are.\n**Save** in the Theme panel folds your dials into the open theme; **Load**\napplies whatever a theme carries, and turns the effect off for a theme that\ncarries none.\n\nUntil you save, the dials sit in your browser only. The Theme panel calls\nthat state off the theme, the same word it uses for an unsaved component\nchange. The built-in **Motion Proto** theme is read-only, so Save\nis disabled there; use **Save As** to fold the dials into a theme of your\nown.\n\n**Save as sketchstyle…** in the **Sketchstyle** view is a different gesture. It\nwrites a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a look you\ncan pick from any theme. It never touches the open theme, and it never marks\nthe look off the theme.\n\n## Shipping the layer\n\nThe dev server reads the open theme and paints whatever it carries. A built site\nhas no server to ask, so it hands the field over itself:\n\n```ts\nimport { seedSketchFromTheme } from '@motion-proto/live-tokens/sketch';\nimport theme from './live-tokens/data/themes/sketchy.json';\n\nseedSketchFromTheme(theme.sketchStyle);\nawait bootLiveTokens(App, '#app');\n```\n\nCall it before mounting, so the look is up on the first frame. Pass the field\nraw. A theme written against older dial names is carried forward on the way in,\nthe same reconciliation the dev server runs on every theme it reads.\n\nNothing is baked. `tokens.generated.css` still holds token values only, and the\nlayer stays JavaScript the page runs, because it builds an SVG filter bank\nrather than a set of custom properties.\n\nA visitor who has picked a look of their own keeps it, None included. The theme\nseeds a browser that has decided nothing and never overwrites one that has, so\ncalling this on every boot is safe.\n\nIf your site offers a sketch picker, `themeSketchLook` is the theme's own look\nas one more row. It is null when the theme carries none, and null when what it\ncarries is one of the shipped seven, since that row already names it. Give the\nrow `setSketch(look.id)` like any other, and a visitor who wanders off the\ntheme's look can come back to it.\n\n## Drawing your own elements\n\nThe layer draws a fixed set of parts: the shipped components, and four classes\nit reserves for you. Nothing else is touched, so a page element or a\nconsumer-authored component is left crisp until it carries one of them.\n\n| Class | For |\n|---------------------|-----------------------------------------------------------|\n| `sketch-surface` | A box. The default treatment. |\n| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |\n| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |\n| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |\n\nPick by size, not by kind: a card and a modal both take `sketch-container`, a\nbadge and a pill both take `sketch-chip`.\n\nThe class opts the element in; it names no colours, so the element states its\nown. `--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color`,\n`--sketch-radius` and `--sketch-shadow` name the fill, the outline, the hatching\nink, the corners and the shadow for one element and everything inside it. The\nlayer blanks the real background and border, so an element whose fill matters\nunder Sketch mode has to name it here as well as paint it.\n\nThe layer also paints on the element's `::before` and `::after`, forces its\n`overflow` visible, and gives it a stacking context of its own. Keep the class\noff anything that owns a pseudo-element, clips its content, or is positioned\nabsolutely, and put it on a wrapper instead.\n\n```css\n.my-callout {\n background: var(--surface-brand-lowest);\n border: var(--border-width-1) solid var(--border-brand);\n border-radius: var(--radius-xl);\n\n --sketch-fill: var(--surface-brand-lowest);\n --sketch-stroke: var(--border-brand);\n --sketch-radius: var(--radius-xl);\n}\n```\n\nA gradient is a valid fill: the shorthand's last layer takes a colour or an\nimage, so `--sketch-fill` accepts either. States work the same way, since\nnothing is competing with you for the value:\n\n```css\n.my-callout:hover { --sketch-stroke: var(--border-brand-strong); }\n```\n\n## Images inside a drawn part\n\nA drawn part's `overflow` is forced visible, because the fill and outline are\npainted on pseudo-elements that travel past the box and would otherwise be cut\noff at its edge. A background that bleeds is the effect working. An image that\nbleeds is not: it keeps its square corners while the card around it turns.\n\nMedia that runs to a part's edge therefore has to carry that part's corners\nitself. `--sketch-radius` is the radius the layer drew, and it inherits, so a\nchild can read it and fall back to its own value when Sketch mode is off:\n\n```css\n.cover {\n overflow: hidden;\n border-top-left-radius: var(--sketch-radius, var(--card-default-radius));\n border-top-right-radius: var(--sketch-radius, var(--card-default-radius));\n}\n```\n\nCorner spread is per-corner and per-instance, so at high spread the crop is the\nmean rather than an exact trace of the drawn edge.\n\nA rule made from a `border` is not a box and cannot be displaced. Make it an\nelement, give it `sketch-rule`, and name its ink:\n\n```html\n<div class=\"rule sketch-rule\"></div>\n```\n```css\n.rule {\n height: var(--border-width-2);\n background: var(--border-brand);\n --sketch-fill: var(--border-brand);\n}\n```\n\nIcons and inline SVG take the wobble directly, since a glyph has no box to\nredraw. Body type is left alone: an icon is a shape and survives a wobble, a\nparagraph is not.\n\n`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It\ninherits, so one declaration covers everything under it, and it takes the ink\nmask off as well as the wobble:\n\n```css\n/* Crisp. Chrome, a logo, anything that has to stay exact. */\n.app-bar { --sketch-icon-off: none; }\n\n/* Drawn back rather than off, at a third of the travel. Small artwork, and\n type set as an SVG, which the layer reads as one large glyph. */\n.wordmark { --sketch-icon-off: var(--sketch-icon-soft); }\n```\n\nThe **Blotch size** dial under Icons and SVG is a share of the glyph rather than\na px size, because no px size is right for both a 16px icon and a page-wide\ndrawing. At 100% every glyph gets one period of the field across it whatever its\nsize. Below that the field repeats inside the glyph and the blotches get finer.\nAbove it a glyph reads part of one blotch, so the mask thins the whole glyph\nunevenly instead of breaking it up. The fill's blotches stay in px, since a\ncomponent does have a size to state one against.\n",
11
11
  "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, a setting for every component, and the sketch layer, in one\nfile. It carries the name the look ships under, whether production is running\nit, and **Adopt**. Three parts sit under it, each a read-out rather than a\nfile 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 have an unsaved edit that has not\n been saved into the theme, and opens the component editors.\n- **Sketchstyle** names the look the theme's sketch layer carries: its label, or\n off the theme when what's on screen no longer matches what was saved, or\n none when the theme carries no sketch layer. It travels with the theme\n like colors and type do, but never reaches a production build.\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, a setting for every component, and the\n sketch layer.\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, writing most parts to a buffer,\n `_working.json`, one slot each; the sketch layer has no buffer and stays\n live in the browser until you save. **Save** captures all of it into the\n open theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`. **Adopt** changes it; saving a preset in the Theme\n Picker performs that Adopt for you.\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## Fonts\n\nType is part of the look, so it saves, loads and ships with the theme rather\nthan on its own. Four named stacks carry it:\n\n| Stack | Used by |\n|---|---|\n| `--font-display` | headings |\n| `--font-sans` | body text and most UI |\n| `--font-serif` | anywhere you ask for it |\n| `--font-mono` | code |\n\nEach stack is a family followed by its fallbacks, so a page still reads while a\nweb font loads, and still reads if it never does. **Project fonts**, in the\nColors and type editor, is where families come from: type a Google Fonts family\nname and the editor checks it, or paste a fonts URL, an embed tag, or your own\n`@font-face` rules. Removing a family puts the stack back on its fallbacks.\n\nYou can also set both faces at once from the command line:\n\n```bash\nnpx live-tokens set-fonts fonts.json\n```\n\nwith a brief naming the families:\n\n```json\n{ \"display\": \"Fraunces\", \"body\": \"Nunito Sans\" }\n```\n\nIt checks each family against Google Fonts, works out the weights that family\nactually has, and binds it to its stack. Like every other edit, the result lands\nin the buffer, so **Save** keeps it. In Claude Code, asking for a font pairing in\nplain English runs the same command.\n\nA font is only requested by the browser once something on the page uses it, so\ncarrying a family you no longer reference costs nothing at load. Adopting is\nwhat writes the font imports your site ships, into `fonts.css`.\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 editors keep their own unsaved state. If one or more components are\nwaiting when you use **Save**, **Save As**, or **Adopt**, the Theme panel offers\nto save all of them before continuing. You can accept once instead of visiting\neach component, or cancel to review them individually. A component editor's\n**Save As** creates a reusable component preset.\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**—or clicking the active theme's name—opens the Theme Picker. Picking a\ntheme shows it on the page as a preview with nothing written to disk, sketch\nlayer included, so you can try each look and compare. **Save** in that window\nopens and adopts the previewed theme in one step: the active pointer changes,\nthe buffers clear, the editor works on it, and production ships it. **Cancel**\nreturns you to where you were, unsaved sketch dials included. Previewing alone\nnever 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 and your sketch layer stay as they are, and\nthe theme you have open stays open. Saved colors and type files are listed\nthere too, marked *colors & type*, and picking one is always that narrower\nload.\n\n## Shipping\n\n**Adopt**, in the Theme panel, is the \"ship it\" step. It saves the open theme,\nthen bakes the colors and type plus every component the theme carries into\n`src/live-tokens/data/tokens.generated.css`, which your build bundles alongside\n`tokens.css`. The sketch layer is the one part of the theme Adopt never bakes:\nit stays a preview. Fonts regenerate to match. The line under the theme name\nsays whether production is running 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 save-then-bake 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",
12
12
  "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 sketch-styles/\n my-look.json a sketchstyle saved from the Sketchstyle view\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, a\nsetting for every component, and the sketch layer. It depends on no other\nfile, so deleting 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. When the Theme panel finds several dirty\n components, **Save all** writes those buffers together.\n- **Save** captures the buffers into the open theme's file, along with the\n sketch layer, which has no buffer of its own and lives only in the browser\n until Save writes it. That file is the durable copy of your look; matching\n 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\nProjects upgraded from 0.48 may initially contain working files copied from the\nactive theme. On the first dev-server boot, exact copies are removed\nautomatically. Any file that differs is kept as unsaved work, so no migration\ncommand is required.\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",
13
13
  };