@motion-proto/live-tokens 0.72.1 → 0.74.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.
Files changed (81) hide show
  1. package/.claude/skills/live-tokens-build-page/SKILL.md +37 -2
  2. package/.claude/skills/live-tokens-build-page/references/layout-sources.md +48 -0
  3. package/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -2
  4. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -4
  5. package/.claude/skills/live-tokens-create-theme/SKILL.md +80 -0
  6. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
  7. package/.claude/skills/live-tokens-pick-component/SKILL.md +1 -1
  8. package/.claude/skills/live-tokens-set-colors/SKILL.md +140 -0
  9. package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
  10. package/.claude/skills/{live-tokens-adjust-geometry → live-tokens-set-geometry}/SKILL.md +12 -7
  11. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +61 -0
  12. package/.claude/skills/{live-tokens-pair-fonts → live-tokens-set-type}/SKILL.md +18 -15
  13. package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
  14. package/CHANGELOG.md +141 -0
  15. package/README.md +24 -15
  16. package/bin/check-page.mjs +3 -3
  17. package/bin/cli.mjs +92 -55
  18. package/bin/lib/liveState.mjs +110 -0
  19. package/bin/save-theme.mjs +177 -0
  20. package/bin/set-colors.mjs +191 -0
  21. package/bin/{adjust.mjs → set-geometry.mjs} +18 -50
  22. package/bin/{set-fonts.mjs → set-type.mjs} +21 -55
  23. package/dist-plugin/{chunk-RIXO2E55.js → chunk-7VRTBGJT.js} +1 -1
  24. package/dist-plugin/{chunk-YLCOIGQC.js → chunk-V3YF6CGT.js} +56 -2
  25. package/dist-plugin/index.cjs +58 -3
  26. package/dist-plugin/index.js +11 -11
  27. package/dist-plugin/migrateData/index.cjs +56 -1
  28. package/dist-plugin/migrateData/index.js +2 -2
  29. package/dist-plugin/{generateColorsAndType → setColors}/index.cjs +1109 -1071
  30. package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +33 -24
  31. package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +33 -24
  32. package/dist-plugin/{generateColorsAndType → setColors}/index.js +45 -63
  33. package/dist-plugin/{adjust → setGeometry}/index.cjs +60 -5
  34. package/dist-plugin/{adjust → setGeometry}/index.d.cts +1 -1
  35. package/dist-plugin/{adjust → setGeometry}/index.d.ts +1 -1
  36. package/dist-plugin/{adjust → setGeometry}/index.js +1 -1
  37. package/dist-plugin/{fontPairing → setType}/index.cjs +4 -4
  38. package/dist-plugin/{fontPairing → setType}/index.d.cts +1 -1
  39. package/dist-plugin/{fontPairing → setType}/index.d.ts +1 -1
  40. package/dist-plugin/{themeTypes-DSV3Zisf.d.cts → themeTypes-BxRtuN5V.d.cts} +1 -1
  41. package/dist-plugin/{themeTypes-DSV3Zisf.d.ts → themeTypes-BxRtuN5V.d.ts} +1 -1
  42. package/package.json +10 -2
  43. package/src/editor/core/themes/{generateColorsAndType.ts → buildColors.ts} +82 -96
  44. package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
  45. package/src/editor/core/themes/migrations/index.ts +2 -0
  46. package/src/editor/docs/content/creating-components.md +13 -0
  47. package/src/editor/docs/content/themes-workflow.md +2 -2
  48. package/src/editor/docs/content.generated.ts +2 -2
  49. package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
  50. package/src/editor/skill-atlas/SkillAtlas.svelte +836 -0
  51. package/src/editor/skill-atlas/SkillAtlas.svelte.d.ts +4 -0
  52. package/src/editor/skill-atlas/SourcePane.svelte +364 -0
  53. package/src/editor/skill-atlas/TreeNodeCard.svelte +206 -0
  54. package/src/editor/skill-atlas/skillSources.generated.ts +45 -0
  55. package/src/editor/skill-atlas/skillSources.ts +1 -0
  56. package/src/editor/skill-atlas/skillTrees.ts +3844 -0
  57. package/src/editor/skill-atlas/types.ts +65 -0
  58. package/src/live-tokens/data/colors-and-type/autumn.json +1 -37
  59. package/src/live-tokens/data/colors-and-type/default.json +1 -37
  60. package/src/live-tokens/data/colors-and-type/halloween.json +1 -37
  61. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -37
  62. package/src/live-tokens/data/colors-and-type/ocean.json +1 -37
  63. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -37
  64. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -37
  65. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -37
  66. package/src/live-tokens/data/colors-and-type/sunset.json +1 -37
  67. package/src/live-tokens/data/themes/autumn.json +1 -37
  68. package/src/live-tokens/data/themes/halloween.json +1 -37
  69. package/src/live-tokens/data/themes/midnight-study.json +1 -37
  70. package/src/live-tokens/data/themes/ocean.json +1 -37
  71. package/src/live-tokens/data/themes/royal-velvet.json +1 -37
  72. package/src/live-tokens/data/themes/sketchy.json +1 -37
  73. package/src/live-tokens/data/themes/spring-meadow.json +1 -37
  74. package/src/live-tokens/data/themes/sunset.json +1 -37
  75. package/src/live-tokens/data/tokens.generated.css +0 -36
  76. package/.claude/skills/live-tokens-generate-theme/SKILL.md +0 -156
  77. package/.claude/skills/live-tokens-generate-theme/references/mood-vocabulary.md +0 -43
  78. package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +0 -18
  79. package/.claude/skills/live-tokens-generate-theme/references/style-vocabulary.md +0 -35
  80. package/bin/generate-theme.mjs +0 -260
  81. /package/dist-plugin/{fontPairing → setType}/index.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.72.1",
3
+ "version": "0.74.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -50,6 +50,7 @@
50
50
  "src/live-tokens/data/sketch-styles/dry.json",
51
51
  "dist-plugin",
52
52
  ".claude/skills",
53
+ "!.claude/skills/**/skill-alt.md",
53
54
  "bin",
54
55
  "template",
55
56
  "!**/*.test.ts",
@@ -101,6 +102,11 @@
101
102
  "svelte": "./src/editor/docs/Docs.svelte",
102
103
  "default": "./src/editor/docs/Docs.svelte"
103
104
  },
105
+ "./skill-atlas": {
106
+ "types": "./src/editor/skill-atlas/SkillAtlas.svelte.d.ts",
107
+ "svelte": "./src/editor/skill-atlas/SkillAtlas.svelte",
108
+ "default": "./src/editor/skill-atlas/SkillAtlas.svelte"
109
+ },
104
110
  "./backdrop": {
105
111
  "svelte": "./src/system/backdrop/index.ts",
106
112
  "types": "./src/system/backdrop/index.ts",
@@ -154,15 +160,17 @@
154
160
  "check:token-contract": "node scripts/check-token-contract.mjs",
155
161
  "check:preset-themes": "node scripts/check-preset-themes.mjs",
156
162
  "check:skills": "node scripts/check-skills.mjs",
163
+ "check:skill-sources": "node scripts/sync-skill-sources.mjs --check",
157
164
  "check:skill-atlas": "node scripts/sync-skill-atlas.mjs",
158
165
  "sync:component-defaults": "node scripts/sync-component-defaults.mjs --write",
159
166
  "sync:docs": "node scripts/sync-docs.mjs --write",
167
+ "sync:skill-sources": "node scripts/sync-skill-sources.mjs --write",
160
168
  "sync:skill-atlas": "node scripts/sync-skill-atlas.mjs --write",
161
169
  "seed:preset-theme": "node scripts/seed-preset-theme.mjs",
162
170
  "collapse:theme": "node scripts/collapse-theme-to-default.mjs",
163
171
  "check:smoke-install": "bash scripts/smoke-install.sh",
164
172
  "check:smoke-create": "bash scripts/smoke-create.sh",
165
- "prepublishOnly": "npm run check:no-style-imports && npm run check:no-tooling-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:pages && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:preset-themes && npm run check:skills && npm run check:skill-atlas && npm run check:smoke-install && npm run check:smoke-create"
173
+ "prepublishOnly": "npm run check:no-style-imports && npm run check:no-tooling-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:pages && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:preset-themes && npm run check:skills && npm run check:skill-sources && npm run check:skill-atlas && npm run check:smoke-install && npm run check:smoke-create"
166
174
  },
167
175
  "peerDependencies": {
168
176
  "@sveltejs/vite-plugin-svelte": "^7.0",
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Brief → generator: a full ColorsAndType from 10 OKLCH seeds + a scheme, with
3
- * AA floors enforced on derived text tokens. Seed *selection* lives in the
4
- * live-tokens-generate-theme skill; the CLI reaches this via
5
- * `dist-plugin/generateColorsAndType`, so keep this module Node-safe (no DOM).
2
+ * Base colors → the theme's color state: ten OKLCH base colors + a scheme give
3
+ * the palettes, the harmony axes, the swatch gradients, and the override bag,
4
+ * with AA floors enforced on derived text tokens. Choosing the base colors
5
+ * lives in the live-tokens-set-colors skill; the CLI reaches this via
6
+ * `dist-plugin/setColors`, so keep this module Node-safe (no DOM).
7
+ *
8
+ * This is one dimension of a theme, not a theme: the caller merges the result
9
+ * into the live colors and type, and `save-theme` composes the document.
6
10
  */
7
11
 
8
12
  import { hexToOklch, cssColorToOklch, oklchToHexClamped, type Oklch } from '../palettes/oklch';
@@ -22,30 +26,26 @@ import { SHADOW_VAR_NAMES, parseShadowCss, shadowTokenCss } from './parsers/shad
22
26
  import { AA_BODY, AA_LARGE, contrastRatio, findLForContrast } from '../palettes/contrast';
23
27
  import { type HarmonyAxis, type HarmonyMode } from '../palettes/colorHarmony';
24
28
  import { defaultPaletteConfig } from '../../ui/palette/paletteMath';
25
- import { sanitizeFileName } from '../storage/files/versionedFileResourceClient';
26
- import { CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION } from './migrations/index';
27
- import type { FontSource, FontStack, GradientDiskToken, PaletteConfig, ColorsAndType } from './themeTypes';
29
+ import type { GradientDiskToken, PaletteConfig } from './themeTypes';
28
30
 
29
- export interface ColorsAndTypeBrief {
30
- name: string;
31
+ export interface ColorsInput {
31
32
  scheme: SchemeDirection;
32
- /** One seed per palette label (all 10 required); hex string or numeric OKLCH. */
33
- seeds: Record<string, Oklch | string>;
34
- /** Advisory record of the harmony reasoning; seeds stay ground truth. */
33
+ /** The one color each palette's ramp derives from (all 10 required); hex string or numeric OKLCH. */
34
+ baseColors: Record<string, Oklch | string>;
35
+ /** Advisory record of the harmony reasoning; the base colors stay ground truth. */
35
36
  harmony?: { mode: HarmonyMode };
36
37
  /** Page-background sky. Off by default; the skill turns it on only when the
37
- * mood brief evokes atmosphere. The engine derives the stops itself, on the
38
+ * request evokes atmosphere. The engine derives the stops itself, on the
38
39
  * scheme's safe side of the Canvas anchor. */
39
40
  canvasGradient?: boolean;
40
41
  }
41
42
 
42
- /** Non-color content carried forward from the currently active colors and type
43
- * so gradients, shadow geometry, component aliases, and fonts survive
44
- * regeneration. Shadow opacity is derived, not carried — see `shadowVars`. */
43
+ /** The live colors and type this pass reads before it replaces the color
44
+ * state: the override bag, so every name no palette owns rides through, and
45
+ * the swatch gradients, so user-tuned ones survive. Shadow opacity is
46
+ * derived, not carried — see `shadowVars`. */
45
47
  export interface CarryForward {
46
48
  cssVariables?: Record<string, string>;
47
- fontSources?: FontSource[];
48
- fontStacks?: FontStack[];
49
49
  gradients?: GradientDiskToken[];
50
50
  }
51
51
 
@@ -59,14 +59,14 @@ export interface ContrastCheck {
59
59
  corrected: boolean;
60
60
  }
61
61
 
62
- export interface GenerateColorsAndTypeReport {
62
+ export interface BuildColorsReport {
63
63
  scheme: SchemeDirection;
64
64
  checks: ContrastCheck[];
65
65
  failures: string[];
66
66
  /** 'recipes' when the engine replaced absent/stock swatch gradients with the
67
67
  * family-relative recipes; 'carried' when user-tuned ones rode through. */
68
68
  gradients: 'recipes' | 'carried';
69
- /** Present when the brief opted into the page-background sky: either
69
+ /** Present when the input opted into the page-background sky: either
70
70
  * 'on, <sky> → <anchor>' or a 'skipped — …' explanation when the Canvas
71
71
  * anchors at the ramp edge and leaves no room. */
72
72
  canvasGradient?: string;
@@ -74,10 +74,18 @@ export interface GenerateColorsAndTypeReport {
74
74
  shadows: string;
75
75
  }
76
76
 
77
- export interface GenerateColorsAndTypeResult {
78
- colorsAndType: ColorsAndType;
79
- slug: string;
80
- report: GenerateColorsAndTypeReport;
77
+ /** The color dimension of a colors-and-type document. Every other key —
78
+ * name, timestamps, fonts, schema version — belongs to the caller. */
79
+ export interface ColorsState {
80
+ editorConfigs: Record<string, PaletteConfig>;
81
+ harmonyAxes: HarmonyAxis[];
82
+ gradients: GradientDiskToken[];
83
+ cssVariables: Record<string, string>;
84
+ }
85
+
86
+ export interface BuildColorsResult {
87
+ colors: ColorsState;
88
+ report: BuildColorsReport;
81
89
  }
82
90
 
83
91
  const HEX_RE = /^#[0-9a-f]{6}$/i;
@@ -112,7 +120,7 @@ const FUNCTIONAL_TEXT_LABELS = PALETTE_SPECS
112
120
 
113
121
  const norm = (h: number): number => (h >= 0 && h < 360 ? h : ((h % 360) + 360) % 360);
114
122
 
115
- function parseSeed(label: string, raw: Oklch | string, problems: string[]): Oklch | null {
123
+ function parseBaseColor(label: string, raw: Oklch | string, problems: string[]): Oklch | null {
116
124
  if (typeof raw === 'string') {
117
125
  if (!HEX_RE.test(raw.trim())) {
118
126
  problems.push(`${label}: "${raw}" is not a #rrggbb hex color`);
@@ -128,41 +136,37 @@ function parseSeed(label: string, raw: Oklch | string, problems: string[]): Oklc
128
136
  return { l, c, h: norm(h) };
129
137
  }
130
138
 
131
- function validateBrief(brief: ColorsAndTypeBrief): { seeds: Record<string, Oklch>; slug: string } {
139
+ function validateInput(input: ColorsInput): Record<string, Oklch> {
132
140
  const problems: string[] = [];
133
- const name = typeof brief.name === 'string' ? brief.name.trim() : '';
134
- if (!name) problems.push('name: required');
135
- const slug = sanitizeFileName(name || 'unnamed');
136
- if (slug === 'default') problems.push('name: "default" is the protected package theme; pick another name');
137
- if (brief.scheme !== 'light' && brief.scheme !== 'dark') {
138
- problems.push(`scheme: must be "light" or "dark", got ${JSON.stringify(brief.scheme)}`);
141
+ if (input.scheme !== 'light' && input.scheme !== 'dark') {
142
+ problems.push(`scheme: must be "light" or "dark", got ${JSON.stringify(input.scheme)}`);
139
143
  }
140
- if (brief.harmony && !HARMONY_MODES.includes(brief.harmony.mode)) {
141
- problems.push(`harmony.mode: unknown mode ${JSON.stringify(brief.harmony.mode)}`);
144
+ if (input.harmony && !HARMONY_MODES.includes(input.harmony.mode)) {
145
+ problems.push(`harmony.mode: unknown mode ${JSON.stringify(input.harmony.mode)}`);
142
146
  }
143
- if (brief.canvasGradient !== undefined && typeof brief.canvasGradient !== 'boolean') {
144
- problems.push(`canvasGradient: must be a boolean, got ${JSON.stringify(brief.canvasGradient)}`);
147
+ if (input.canvasGradient !== undefined && typeof input.canvasGradient !== 'boolean') {
148
+ problems.push(`canvasGradient: must be a boolean, got ${JSON.stringify(input.canvasGradient)}`);
145
149
  }
146
150
 
147
- const seeds: Record<string, Oklch> = {};
148
- const given = brief.seeds ?? {};
151
+ const baseColors: Record<string, Oklch> = {};
152
+ const given = input.baseColors ?? {};
149
153
  for (const spec of PALETTE_SPECS) {
150
154
  if (!(spec.label in given)) {
151
- problems.push(`seeds.${spec.label}: missing`);
155
+ problems.push(`baseColors.${spec.label}: missing`);
152
156
  continue;
153
157
  }
154
- const parsed = parseSeed(spec.label, given[spec.label], problems);
155
- if (parsed) seeds[spec.label] = parsed;
158
+ const parsed = parseBaseColor(spec.label, given[spec.label], problems);
159
+ if (parsed) baseColors[spec.label] = parsed;
156
160
  }
157
161
  const known = new Set<string>(PALETTE_SPECS.map((s) => s.label));
158
162
  for (const label of Object.keys(given)) {
159
- if (!known.has(label)) problems.push(`seeds.${label}: unknown palette (expected ${[...known].join(', ')})`);
163
+ if (!known.has(label)) problems.push(`baseColors.${label}: unknown palette (expected ${[...known].join(', ')})`);
160
164
  }
161
165
 
162
166
  if (problems.length > 0) {
163
- throw new Error(`Invalid theme brief:\n ${problems.join('\n ')}`);
167
+ throw new Error(`Invalid base color file:\n ${problems.join('\n ')}`);
164
168
  }
165
- return { seeds, slug };
169
+ return baseColors;
166
170
  }
167
171
 
168
172
  interface CheckDef {
@@ -233,16 +237,16 @@ function worstSurface(
233
237
  }
234
238
 
235
239
  /** Pin the palette's Text curves so `step` derives to the solved color: the
236
- * lightness anchor is the solved L as a multiplier of seed L, the saturation
237
- * anchor the solved chroma as a multiplier of seed C (the solver may have
240
+ * lightness anchor is the solved L as a multiplier of the base color's L, the
241
+ * saturation anchor the solved chroma as a multiplier of its C (the solver may have
238
242
  * decayed chroma to reach the ratio, so lightness alone can undershoot). */
239
243
  function pinTextStep(cfg: PaletteConfig, step: Step, solved: { l: number; c: number }): void {
240
244
  const x = scaleStepToX(step, TEXT_SCALE);
241
- const seed = cfg.baseColor;
242
- const yL = Math.max(0, Math.min(200, (solved.l / seed.l) * 100));
245
+ const base = cfg.baseColor;
246
+ const yL = Math.max(0, Math.min(200, (solved.l / base.l) * 100));
243
247
  cfg.scaleCurves.Text.lightness = setCurveAnchor(cfg.scaleCurves.Text.lightness, x, yL).curve;
244
- if (seed.c > 0) {
245
- const yC = Math.max(0, Math.min(200, (solved.c / seed.c) * 100));
248
+ if (base.c > 0) {
249
+ const yC = Math.max(0, Math.min(200, (solved.c / base.c) * 100));
246
250
  cfg.scaleCurves.Text.saturation = setCurveAnchor(cfg.scaleCurves.Text.saturation, x, yC).curve;
247
251
  }
248
252
  }
@@ -252,7 +256,7 @@ const MAX_CORRECTION_ROUNDS = 3;
252
256
  function runContrastGate(
253
257
  configs: Record<string, PaletteConfig>,
254
258
  scheme: SchemeDirection,
255
- ): Pick<GenerateColorsAndTypeReport, 'scheme' | 'checks' | 'failures'> {
259
+ ): Pick<BuildColorsReport, 'scheme' | 'checks' | 'failures'> {
256
260
  const defs = checkDefs();
257
261
  const direction = scheme === 'light' ? 'darker' : 'lighter';
258
262
  const corrected = new Set<string>();
@@ -267,14 +271,14 @@ function runContrastGate(
267
271
  const worst = worstSurface(textHex, vars);
268
272
  if (worst.ratio >= def.floor) continue;
269
273
  const cfg = configs[def.paletteLabel];
270
- const seed = cfg.baseColor;
274
+ const base = cfg.baseColor;
271
275
  const solved = findLForContrast({
272
276
  against: varToHex(vars[worst.against]) ?? '#000000',
273
277
  ratio: def.target,
274
278
  direction,
275
279
  c: text.c,
276
- h: seed.h,
277
- lMax: Math.min(1, 2 * seed.l),
280
+ h: base.h,
281
+ lMax: Math.min(1, 2 * base.l),
278
282
  });
279
283
  pinTextStep(cfg, def.step, solved);
280
284
  corrected.add(def.textVar);
@@ -301,8 +305,8 @@ function runContrastGate(
301
305
  .map((c) => {
302
306
  const label = defs.find((d) => d.textVar === c.textVar)!.paletteLabel;
303
307
  const hint = scheme === 'dark'
304
- ? `raise the ${label} seed lightness (derived text tops out at 2× seed L)`
305
- : `raise the ${label} seed lightness or reduce its chroma`;
308
+ ? `raise the ${label} base color lightness (derived text tops out at 2× its L)`
309
+ : `raise the ${label} base color lightness or reduce its chroma`;
306
310
  return `${c.textVar} reaches ${c.ratio.toFixed(2)}:1 vs ${c.against} (floor ${c.floor}:1) — ${hint}`;
307
311
  });
308
312
 
@@ -323,19 +327,19 @@ const linear = (variable: string, angle: number, from: string, to: string): Grad
323
327
  stops: [{ position: 0, color: from }, { position: 100, color: to }],
324
328
  });
325
329
 
326
- /** The four swatch recipes, family-relative so they suit any seed set: a
330
+ /** The four swatch recipes, family-relative so they suit any set of base colors: a
327
331
  * within-family brand sweep, two cross-family pairs that fall back to
328
332
  * within-family when the hues sit more than 120° apart (an srgb gradient
329
333
  * between distant hues passes through gray), and a canvas sweep on the
330
334
  * scheme's rich half of the ramp. Stops are var() refs, so later palette
331
335
  * edits flow through. */
332
- function recipeGradients(seeds: Record<string, Oklch>, scheme: SchemeDirection): GradientDiskToken[] {
336
+ function recipeGradients(baseColors: Record<string, Oklch>, scheme: SchemeDirection): GradientDiskToken[] {
333
337
  return [
334
338
  linear('--gradient-1', 90, '--color-brand-400', '--color-brand-700'),
335
- hueDist(seeds.Special.h, seeds.Brand.h) <= ADJACENT_HUE_MAX
339
+ hueDist(baseColors.Special.h, baseColors.Brand.h) <= ADJACENT_HUE_MAX
336
340
  ? linear('--gradient-2', 135, '--color-special-500', '--color-brand-500')
337
341
  : linear('--gradient-2', 135, '--color-special-400', '--color-special-700'),
338
- hueDist(seeds.Brand.h, seeds.Accent.h) <= ADJACENT_HUE_MAX
342
+ hueDist(baseColors.Brand.h, baseColors.Accent.h) <= ADJACENT_HUE_MAX
339
343
  ? linear('--gradient-3', 90, '--color-brand-500', '--color-accent-500')
340
344
  : linear('--gradient-3', 90, '--color-accent-400', '--color-accent-700'),
341
345
  scheme === 'dark'
@@ -367,7 +371,7 @@ function applyCanvasGradient(canvas: PaletteConfig, scheme: SchemeDirection): st
367
371
  ? Math.min(anchor + 2, labels.length - 1)
368
372
  : Math.max(anchor - 2, 0);
369
373
  if (sky === anchor) {
370
- return `skipped — the Canvas seed anchors at the ramp edge (${labels[anchor]}), leaving no room for a sky; commit the canvas further from ${scheme === 'dark' ? 'black' : 'white'}`;
374
+ return `skipped — the Canvas base color anchors at the ramp edge (${labels[anchor]}), leaving no room for a sky; commit the canvas further from ${scheme === 'dark' ? 'black' : 'white'}`;
371
375
  }
372
376
  canvas.emptyMode = 'gradient';
373
377
  canvas.gradientStyle = 'linear';
@@ -409,9 +413,9 @@ export function shadowOpacityForCanvas(canvasL: number): number {
409
413
 
410
414
  /** Recolor the scale for this canvas: carried geometry and color ride through
411
415
  * (elevation shape belongs to the shape pass), opacity is replaced. Replaced
412
- * rather than kept-when-untouched, because a brief iterated from a light
416
+ * rather than kept-when-untouched, because a run iterated from a light
413
417
  * canvas to a dark one would otherwise hold a shadow too faint to see. The
414
- * Canvas seed is the page background verbatim, so it is the ground the
418
+ * Canvas base color is the page background verbatim, so it is the ground the
415
419
  * shadow falls on. */
416
420
  function shadowVars(carried: Record<string, string>, canvas: Oklch): Record<string, string> {
417
421
  const opacity = shadowOpacityForCanvas(canvas.l);
@@ -424,36 +428,32 @@ function shadowVars(carried: Record<string, string>, canvas: Oklch): Record<stri
424
428
  return out;
425
429
  }
426
430
 
427
- export function buildColorsAndTypeFromSeeds(
428
- brief: ColorsAndTypeBrief,
429
- carry: CarryForward = {},
430
- now: string = new Date().toISOString(),
431
- ): GenerateColorsAndTypeResult {
432
- const { seeds, slug } = validateBrief(brief);
431
+ export function buildColors(input: ColorsInput, carry: CarryForward = {}): BuildColorsResult {
432
+ const baseColors = validateInput(input);
433
433
 
434
434
  const configs: Record<string, PaletteConfig> = {};
435
435
  for (const spec of PALETTE_SPECS) {
436
436
  configs[spec.label] = defaultPaletteConfig({
437
- baseColor: seeds[spec.label],
437
+ baseColor: baseColors[spec.label],
438
438
  neutral: spec.neutral,
439
- scheme: brief.scheme,
439
+ scheme: input.scheme,
440
440
  });
441
441
  }
442
442
 
443
- const report = runContrastGate(configs, brief.scheme);
444
- const canvasGradient = brief.canvasGradient
445
- ? applyCanvasGradient(configs.Canvas, brief.scheme)
443
+ const report = runContrastGate(configs, input.scheme);
444
+ const canvasGradient = input.canvasGradient
445
+ ? applyCanvasGradient(configs.Canvas, input.scheme)
446
446
  : undefined;
447
447
 
448
448
  const gradients = isStockGradients(carry.gradients)
449
- ? recipeGradients(seeds, brief.scheme)
449
+ ? recipeGradients(baseColors, input.scheme)
450
450
  : carry.gradients!;
451
451
 
452
452
  const harmonyAxes: HarmonyAxis[] = [
453
- { hue: norm(seeds.Brand.h), family: 'Brand' },
454
- { hue: norm(seeds.Accent.h), family: 'Accent' },
455
- { hue: norm(seeds.Canvas.h), family: 'Canvas' },
456
- { hue: norm(seeds.Brand.h + 270), family: null },
453
+ { hue: norm(baseColors.Brand.h), family: 'Brand' },
454
+ { hue: norm(baseColors.Accent.h), family: 'Accent' },
455
+ { hue: norm(baseColors.Canvas.h), family: 'Canvas' },
456
+ { hue: norm(baseColors.Brand.h + 270), family: null },
457
457
  ];
458
458
 
459
459
  // The catch-all bag carries only tokens no typed slice owns (the server's
@@ -464,28 +464,14 @@ export function buildColorsAndTypeFromSeeds(
464
464
  );
465
465
  // Rendered projections of the structured gradients, kept for production CSS.
466
466
  for (const t of gradients) cssVariables[t.variable] = formatGradientValue(t);
467
- Object.assign(cssVariables, shadowVars(carry.cssVariables ?? {}, seeds.Canvas));
468
-
469
- const colorsAndType: ColorsAndType = {
470
- name: brief.name.trim(),
471
- createdAt: now,
472
- updatedAt: now,
473
- editorConfigs: configs,
474
- cssVariables,
475
- fontSources: carry.fontSources ?? [],
476
- fontStacks: carry.fontStacks ?? [],
477
- gradients,
478
- harmonyAxes,
479
- schemaVersion: CURRENT_COLORS_AND_TYPE_SCHEMA_VERSION,
480
- };
467
+ Object.assign(cssVariables, shadowVars(carry.cssVariables ?? {}, baseColors.Canvas));
481
468
 
482
469
  return {
483
- colorsAndType,
484
- slug,
470
+ colors: { editorConfigs: configs, harmonyAxes, gradients, cssVariables },
485
471
  report: {
486
472
  ...report,
487
473
  gradients: gradients === carry.gradients ? 'carried' : 'recipes',
488
- shadows: `opacity ${shadowOpacityForCanvas(seeds.Canvas.l)} for a canvas at L ${seeds.Canvas.l.toFixed(2)}`,
474
+ shadows: `opacity ${shadowOpacityForCanvas(baseColors.Canvas.l)} for a canvas at L ${baseColors.Canvas.l.toFixed(2)}`,
489
475
  ...(canvasGradient ? { canvasGradient } : {}),
490
476
  },
491
477
  };
@@ -0,0 +1,62 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * The color, type, and border-width siblings of the shape/space keys
5
+ * 2026-08-13 removed. All 36 were left behind when Badge's `trait` variant,
6
+ * SectionDivider's title and description slots, and Dialog's variant-by-state
7
+ * axes were renamed. `tokens.css` declares none of them and nothing reads one,
8
+ * so they are a dead second home for state the component configs now own.
9
+ */
10
+ const DROPPED = new Set([
11
+ '--badge-trait-surface',
12
+ '--badge-trait-text',
13
+ '--badge-trait-text-font-family',
14
+ '--badge-trait-text-font-size',
15
+ '--badge-trait-text-font-weight',
16
+ '--badge-trait-text-line-height',
17
+ '--badge-trait-border',
18
+ '--badge-trait-border-width',
19
+ '--badge-trait-shadow',
20
+ '--sectiondivider-title',
21
+ '--sectiondivider-title-font-family',
22
+ '--sectiondivider-title-font-size',
23
+ '--sectiondivider-title-font-weight',
24
+ '--sectiondivider-title-line-height',
25
+ '--sectiondivider-title-border-width',
26
+ '--sectiondivider-title-stroke-color',
27
+ '--sectiondivider-description',
28
+ '--sectiondivider-description-font-family',
29
+ '--sectiondivider-description-font-size',
30
+ '--sectiondivider-description-font-weight',
31
+ '--sectiondivider-description-line-height',
32
+ '--dialog-primary-default-surface',
33
+ '--dialog-primary-default-text',
34
+ '--dialog-primary-default-border',
35
+ '--dialog-primary-default-border-width',
36
+ '--dialog-primary-hover-surface',
37
+ '--dialog-primary-hover-text',
38
+ '--dialog-primary-hover-border',
39
+ '--dialog-primary-hover-border-width',
40
+ '--dialog-secondary-default-text',
41
+ '--dialog-secondary-default-border',
42
+ '--dialog-secondary-default-border-width',
43
+ '--dialog-secondary-hover-surface',
44
+ '--dialog-secondary-hover-text',
45
+ '--dialog-secondary-hover-border',
46
+ '--dialog-secondary-hover-border-width',
47
+ ]);
48
+
49
+ export const colorsAndTypeMigration_2026_09_03_dropLegacyComponentKeys: Migration = {
50
+ id: '2026-09-03-drop-legacy-component-keys',
51
+ fromVersion: 7,
52
+ toVersion: 8,
53
+ appliesTo: 'colors-and-type',
54
+ apply(rawVars) {
55
+ const out: Record<string, string> = {};
56
+ for (const [key, value] of Object.entries(rawVars)) {
57
+ if (DROPPED.has(key)) continue;
58
+ out[key] = value;
59
+ }
60
+ return out;
61
+ },
62
+ };
@@ -73,6 +73,7 @@ import {
73
73
  import { componentMigration_2026_09_01_tabbarActiveTint } from './2026-09-01-tabbar-active-tint';
74
74
  import { componentMigration_2026_09_01_gateSuffixEnabled } from './2026-09-01-gate-suffix-enabled';
75
75
  import { componentMigration_2026_09_02_sectiondividerDropTitleOutline } from './2026-09-02-sectiondivider-drop-title-outline';
76
+ import { colorsAndTypeMigration_2026_09_03_dropLegacyComponentKeys } from './2026-09-03-drop-legacy-component-keys';
76
77
 
77
78
  /**
78
79
  * Registered migrations. Order in this array does not matter — the runner
@@ -112,6 +113,7 @@ export const MIGRATIONS: Migration[] = [
112
113
  componentMigration_2026_09_01_tabbarActiveTint,
113
114
  componentMigration_2026_09_01_gateSuffixEnabled,
114
115
  componentMigration_2026_09_02_sectiondividerDropTitleOutline,
116
+ colorsAndTypeMigration_2026_09_03_dropLegacyComponentKeys,
115
117
  ];
116
118
 
117
119
  function countFor(kind: 'colors-and-type' | 'component-config'): number {
@@ -16,6 +16,19 @@ npx @motion-proto/live-tokens setup-claude
16
16
  This copies the bundled skills into your project's `.claude/skills/`. Once
17
17
  they're there, Claude Code picks them up automatically.
18
18
 
19
+ ## Browse the skill atlas
20
+
21
+ The package also ships a Skill Atlas component: a diagram of the reasoning
22
+ behind each bundled skill, with its `SKILL.md` and reference files alongside
23
+ so you can see exactly which lines drive which step. Import it from
24
+ `@motion-proto/live-tokens/skill-atlas` and mount it on a route of your own:
25
+
26
+ ```js
27
+ '/skills': {
28
+ lazy: () => import('@motion-proto/live-tokens/skill-atlas'),
29
+ },
30
+ ```
31
+
19
32
  ## Ask for a component
20
33
 
21
34
  Describe what you want in plain English. Phrases like these trigger the skill:
@@ -63,10 +63,10 @@ name and the editor checks it, or paste a fonts URL, an embed tag, or your own
63
63
  You can also set both faces at once from the command line:
64
64
 
65
65
  ```bash
66
- npx live-tokens set-fonts fonts.json
66
+ npx live-tokens set-type fonts.json
67
67
  ```
68
68
 
69
- with a brief naming the families:
69
+ with a pairing file naming the families:
70
70
 
71
71
  ```json
72
72
  { "display": "Fraunces", "body": "Nunito Sans" }
@@ -3,11 +3,11 @@
3
3
 
4
4
  export const docContent: Record<string, string> = {
5
5
  "01-overview": "# Overview\n\nLiveTokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button,\n IconButton, Card, Dialog, Table, and more). You style components by changing\n the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Sketch mode](sketch-mode.md)**: redraw the page by hand.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
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",
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## Browse the skill atlas\n\nThe package also ships a Skill Atlas component: a diagram of the reasoning\nbehind each bundled skill, with its `SKILL.md` and reference files alongside\nso you can see exactly which lines drive which step. Import it from\n`@motion-proto/live-tokens/skill-atlas` and mount it on a route of your own:\n\n```js\n'/skills': {\n lazy: () => import('@motion-proto/live-tokens/skill-atlas'),\n},\n```\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 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## Washes and gradients\n\n- **Scrims** are translucent layers that dim what sits behind them, like the\n one a dialog draws over the page. Set a colour and opacity per stop.\n- **Tints** are the opposite operation: they shade the surface they sit on\n rather than dimming what is behind it, which is what a hover needs.\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 theme 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
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 sketchstyles 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\nAll seven ship as files, one per sketchstyle, under\n`src/live-tokens/data/sketch-styles/` in the package. There is no sketchstyle that\nexists only as code, so every one of them can be read, copied and edited.\n\nPick one, then move whatever you like. **Save As** keeps your dials under a name\nof your own, alongside the shipped seven, as a file under\n`src/live-tokens/data/sketch-styles/` in your project. **Save** writes them back\nover the sketchstyle you have selected, and lights as soon as the dials leave\nit. On one of your own it writes that file. On a shipped one it writes your\nproject's own copy under the same name, which takes its place in the list;\ndelete that copy and the shipped file behind it comes back. Both are a\ndifferent gesture from saving a theme; see \"Where the settings live\" below.\n\nA theme carries its sketch settings by value, so it can hold a set that belongs\nto no file. The list shows that set as **Unsaved**. It owns no file, so there is\nnothing for Save to write over; **Save As** turns it into a sketchstyle like any\nother, which you can then save over and delete.\n\nYour sketchstyles and the shipped ones are one list. A sketchstyle named after\na shipped one replaces it, keeping its place in the list, so a project that\nwants its own Pencil saves one and every picker shows that one instead.\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 across and down and\n how many levels of detail, then work the field as a levels control. The two\n sizes move together under a chain; break it and the blotches stretch, which\n reads as ink dragged along the axis you widened, and a **Rotation** dial joins\n them to point that stretch anywhere you like. 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** and **Save As** in the **Sketchstyle** view are a different gesture.\nThey write a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a sketchstyle\nyou can pick from any theme. Neither touches the open theme, and neither marks\nthe theme as changed.\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.sketchSettings);\nawait bootLiveTokens(App, '#app');\n```\n\nCall it before mounting, so the sketch layer 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 sketchstyle 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\n### Your own sketchstyles\n\nThe dev server lists the files in `sketch-styles/`. A built site has no server\nto ask, so it hands them over at boot, the way it hands over components:\n\n```ts\nconst files = import.meta.glob<{ name?: string; settings: unknown }>(\n './live-tokens/data/sketch-styles/*.json',\n { eager: true, import: 'default' },\n);\n\nawait bootLiveTokens(App, '#app', {\n sketchStyles: Object.entries(files).map(([path, file]) => {\n const id = path.split('/').pop()!.replace('.json', '');\n return { id, label: file.name || id, settings: file.settings };\n }),\n});\n```\n\nProjects made with `create` ship this already. The file's slug is the sketchstyle's\nid, so a sketchstyle picked in the editor keeps working once the site is built.\n\n### Building a picker\n\n`sketchStyles` is every sketchstyle on offer, shipped and your own, as a store. Give\neach row `setSketch(style.id)`, and add your own **None** row: off is a state of\nthe effect rather than one of the sketchstyles.\n\n`unsavedSketchStyle` is the settings the theme carries as one more row. It is null when the\ntheme carries none, and null when what it carries matches a sketchstyle already in\n`sketchStyles`, since that row names it. Its id goes to `setSketch` like any\nother, so a visitor who wanders off the theme's own settings 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
- "themes-workflow": "# Themes\n\nSave your work, switch between themes, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole theme:\ncolors, type, a setting for every component, and the sketch layer, in one\nfile. It carries the name the theme 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 sketchstyle 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 theme: 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 theme diverges from the active theme, so a newly opened theme has none.\n\n## Fonts\n\nType is part of the theme, 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 theme 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 theme 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 theme, 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 theme\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",
11
+ "themes-workflow": "# Themes\n\nSave your work, switch between themes, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole theme:\ncolors, type, a setting for every component, and the sketch layer, in one\nfile. It carries the name the theme 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 sketchstyle 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 theme: 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 theme diverges from the active theme, so a newly opened theme has none.\n\n## Fonts\n\nType is part of the theme, 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-type fonts.json\n```\n\nwith a pairing file 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 theme 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 theme 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 theme, 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 theme\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 theme, rewritten at boot\n my-brand.json a saved theme: the whole theme 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-style.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 everything 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 theme; 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 themes 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
  };