@motion-proto/live-tokens 0.56.1 → 0.57.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 (90) hide show
  1. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +2 -2
  2. package/CHANGELOG.md +124 -0
  3. package/README.md +1 -1
  4. package/bin/cli.mjs +2 -1
  5. package/dist-plugin/adjust/index.cjs +1 -0
  6. package/dist-plugin/adjust/index.d.cts +2 -2
  7. package/dist-plugin/adjust/index.d.ts +2 -2
  8. package/dist-plugin/adjust/index.js +1 -1
  9. package/dist-plugin/{chunk-TKZBVIW5.js → chunk-E5QYON4L.js} +59 -2
  10. package/dist-plugin/{chunk-PB2JTK2H.js → chunk-LW4SR7AZ.js} +39 -6
  11. package/dist-plugin/{chunk-YR6GPXW2.js → chunk-XWXIMTWZ.js} +0 -5
  12. package/dist-plugin/{chunk-D3ZVKOR4.js → chunk-Y5CNFSSV.js} +1 -0
  13. package/dist-plugin/{dataPaths-DBN0RPuT.d.cts → dataPaths-CRfD1LdA.d.cts} +3 -0
  14. package/dist-plugin/{dataPaths-DBN0RPuT.d.ts → dataPaths-CRfD1LdA.d.ts} +3 -0
  15. package/dist-plugin/fontPairing/index.cjs +4 -2
  16. package/dist-plugin/fontPairing/index.d.cts +3 -3
  17. package/dist-plugin/fontPairing/index.d.ts +3 -3
  18. package/dist-plugin/fontPairing/index.js +4 -3
  19. package/dist-plugin/generateColorsAndType/index.cjs +22 -9
  20. package/dist-plugin/generateColorsAndType/index.d.cts +2 -2
  21. package/dist-plugin/generateColorsAndType/index.d.ts +2 -2
  22. package/dist-plugin/generateColorsAndType/index.js +8 -5
  23. package/dist-plugin/index.cjs +158 -9
  24. package/dist-plugin/index.d.cts +1 -1
  25. package/dist-plugin/index.d.ts +1 -1
  26. package/dist-plugin/index.js +71 -4
  27. package/dist-plugin/migrateData/index.cjs +1 -0
  28. package/dist-plugin/migrateData/index.d.cts +1 -1
  29. package/dist-plugin/migrateData/index.d.ts +1 -1
  30. package/dist-plugin/migrateData/index.js +1 -1
  31. package/dist-plugin/{themeTypes-BAqtv4XO.d.cts → themeTypes-DSV3Zisf.d.cts} +12 -1
  32. package/dist-plugin/{themeTypes-BAqtv4XO.d.ts → themeTypes-DSV3Zisf.d.ts} +12 -1
  33. package/dist-plugin/tokensCssMigrations/index.cjs +58 -1
  34. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  35. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  36. package/dist-plugin/tokensCssMigrations/index.js +3 -3
  37. package/package.json +3 -1
  38. package/src/editor/core/fonts/applyFontPairing.ts +3 -2
  39. package/src/editor/core/fonts/fontLoader.ts +1 -0
  40. package/src/editor/core/fonts/fontMigration.ts +31 -1
  41. package/src/editor/core/palettes/paletteDerivation.ts +9 -2
  42. package/src/editor/core/sketch/maskField.ts +381 -0
  43. package/src/editor/core/sketch/sketchLayer.ts +1028 -0
  44. package/src/editor/core/sketch/sketchPresetService.ts +65 -0
  45. package/src/editor/core/sketch/sketchPresets.ts +328 -0
  46. package/src/editor/core/sketch/sketchStore.ts +190 -0
  47. package/src/editor/core/store/editorPersistence.ts +28 -12
  48. package/src/editor/core/store/editorTypes.ts +4 -1
  49. package/src/editor/core/store/editorViewStore.ts +2 -2
  50. package/src/editor/core/store/gradientSource.ts +15 -1
  51. package/src/editor/core/themes/parsers/gradient.ts +62 -4
  52. package/src/editor/core/themes/slices/gradients.ts +75 -14
  53. package/src/editor/core/themes/themeTypes.ts +6 -1
  54. package/src/editor/docs/Docs.svelte +4 -3
  55. package/src/editor/docs/chapters.ts +1 -0
  56. package/src/editor/docs/content/01-overview.md +2 -1
  57. package/src/editor/docs/content/editing-tokens.md +5 -1
  58. package/src/editor/docs/content/sketch-mode.md +89 -0
  59. package/src/editor/docs/content.generated.ts +3 -2
  60. package/src/editor/overlay/LiveEditorOverlay.svelte +15 -0
  61. package/src/editor/pages/EditorShell.svelte +43 -0
  62. package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
  63. package/src/editor/ui/FontStackEditor.svelte +3 -0
  64. package/src/editor/ui/GradientEditor.svelte +38 -1
  65. package/src/editor/ui/UIReveal.svelte +7 -1
  66. package/src/editor/ui/UISegmentedControl.svelte +4 -8
  67. package/src/editor/ui/sections/GradientsSection.svelte +47 -1
  68. package/src/editor/ui/sketch/SketchDial.svelte +124 -0
  69. package/src/editor/ui/sketch/SketchPreview.svelte +119 -0
  70. package/src/editor/ui/sketch/SketchRange.svelte +185 -0
  71. package/src/editor/ui/sketch/SketchTab.svelte +1263 -0
  72. package/src/live-tokens/data/colors-and-type/autumn.json +17 -0
  73. package/src/live-tokens/data/colors-and-type/default.json +17 -0
  74. package/src/live-tokens/data/colors-and-type/halloween.json +17 -0
  75. package/src/live-tokens/data/colors-and-type/midnight-study.json +17 -0
  76. package/src/live-tokens/data/colors-and-type/ocean.json +17 -0
  77. package/src/live-tokens/data/colors-and-type/royal-velvet.json +17 -0
  78. package/src/live-tokens/data/colors-and-type/sketches.json +2520 -0
  79. package/src/live-tokens/data/colors-and-type/spring-meadow.json +17 -0
  80. package/src/live-tokens/data/colors-and-type/sunset.json +17 -0
  81. package/src/live-tokens/data/themes/autumn.json +17 -0
  82. package/src/live-tokens/data/themes/halloween.json +17 -0
  83. package/src/live-tokens/data/themes/midnight-study.json +17 -0
  84. package/src/live-tokens/data/themes/ocean.json +17 -0
  85. package/src/live-tokens/data/themes/royal-velvet.json +17 -0
  86. package/src/live-tokens/data/themes/sketches.json +3984 -0
  87. package/src/live-tokens/data/themes/spring-meadow.json +17 -0
  88. package/src/live-tokens/data/themes/sunset.json +17 -0
  89. package/src/live-tokens/data/tokens.generated.css +1 -0
  90. package/src/system/styles/tokens.css +15 -2
@@ -1,13 +1,13 @@
1
1
  import { writable } from 'svelte/store';
2
2
 
3
- export type EditorView = 'tokens' | 'components' | 'colors';
3
+ export type EditorView = 'tokens' | 'components' | 'colors' | 'sketch';
4
4
  export type SidebarCondensed = boolean | 'auto';
5
5
 
6
6
  const VIEW_KEY = 'lt.editorView';
7
7
  const CONDENSED_KEY = 'lt.sidebarCondensed';
8
8
 
9
9
  function isEditorView(v: unknown): v is EditorView {
10
- return v === 'tokens' || v === 'components' || v === 'colors';
10
+ return v === 'tokens' || v === 'components' || v === 'colors' || v === 'sketch';
11
11
  }
12
12
 
13
13
  // Session-scoped, not persistent: opening the editor fresh always starts on
@@ -24,13 +24,17 @@ import {
24
24
  setGradientStop,
25
25
  addGradientStop,
26
26
  removeGradientStop,
27
+ setGradientDirection,
27
28
  } from '../themes/slices/gradients';
29
+ import type { LinearDirection } from '../themes/parsers/gradient';
28
30
  import { setComponentAlias } from '../themes/slices/components';
29
31
  import { mutate } from './editorCore';
30
32
 
31
33
  export interface GradientSourceSnapshot {
32
34
  type: GradientType;
33
35
  angle: number;
36
+ /** `to <side-or-corner>` heading; overrides `angle` when set. */
37
+ direction?: LinearDirection;
34
38
  /** Horizontal center for radial gradients, 0–100. */
35
39
  centerX?: number;
36
40
  /** Per-axis stretch factors for the radial ellipse (1 = unscaled). */
@@ -48,6 +52,7 @@ export interface GradientSource {
48
52
  setAll(next: GradientSourceSnapshot): void;
49
53
  setType(type: GradientType): void;
50
54
  setAngle(angle: number): void;
55
+ setDirection(direction: LinearDirection | undefined): void;
51
56
  setCenterX(centerX: number): void;
52
57
  setAspect(aspect: { x: number; y: number }): void;
53
58
  setStop(index: number, partial: Partial<GradientTokenStop>): void;
@@ -63,6 +68,7 @@ export function colorsAndTypeGradientSource(variable: string): GradientSource {
63
68
  return {
64
69
  type: t.type,
65
70
  angle: t.angle,
71
+ direction: t.direction,
66
72
  centerX: t.centerX,
67
73
  aspectX: t.aspectX,
68
74
  aspectY: t.aspectY,
@@ -78,6 +84,7 @@ export function colorsAndTypeGradientSource(variable: string): GradientSource {
78
84
  setCenterX: (x) => setGradientCenterX(variable, x),
79
85
  setAspect: (a) => setGradientAspect(variable, a),
80
86
  setStop: (i, p) => setGradientStop(variable, i, p),
87
+ setDirection: (d) => setGradientDirection(variable, d),
81
88
  addStop: (s) => addGradientStop(variable, s),
82
89
  removeStop: (i) => removeGradientStop(variable, i),
83
90
  };
@@ -130,6 +137,7 @@ export function componentGradientSource(component: string, varName: string): Gra
130
137
  return {
131
138
  type: value.type,
132
139
  angle: value.angle,
140
+ direction: value.direction,
133
141
  centerX: value.centerX,
134
142
  aspectX: value.aspectX,
135
143
  aspectY: value.aspectY,
@@ -151,6 +159,7 @@ export function componentGradientSource(component: string, varName: string): Gra
151
159
  ? {
152
160
  type: ref.value.type,
153
161
  angle: ref.value.angle,
162
+ ...(ref.value.direction !== undefined ? { direction: ref.value.direction } : {}),
154
163
  ...(ref.value.centerX !== undefined ? { centerX: ref.value.centerX } : {}),
155
164
  ...(ref.value.aspectX !== undefined ? { aspectX: ref.value.aspectX } : {}),
156
165
  ...(ref.value.aspectY !== undefined ? { aspectY: ref.value.aspectY } : {}),
@@ -167,6 +176,7 @@ export function componentGradientSource(component: string, varName: string): Gra
167
176
  setAll: (next) => writeComponentGradient(component, varName, {
168
177
  type: next.type,
169
178
  angle: next.angle,
179
+ ...(next.direction !== undefined ? { direction: next.direction } : {}),
170
180
  ...(next.centerX !== undefined ? { centerX: next.centerX } : {}),
171
181
  ...(next.aspectX !== undefined ? { aspectX: next.aspectX } : {}),
172
182
  ...(next.aspectY !== undefined ? { aspectY: next.aspectY } : {}),
@@ -181,7 +191,11 @@ export function componentGradientSource(component: string, varName: string): Gra
181
191
  }
182
192
  g.type = t;
183
193
  }),
184
- setAngle: (a) => update(`set gradient angle ${varName}`, (g) => { g.angle = a; }),
194
+ setAngle: (a) => update(`set gradient angle ${varName}`, (g) => {
195
+ g.angle = a;
196
+ g.direction = undefined;
197
+ }),
198
+ setDirection: (d) => update(`set gradient direction ${varName}`, (g) => { g.direction = d; }),
185
199
  setCenterX: (x) => update(`set gradient center ${varName}`, (g) => { g.centerX = x; }),
186
200
  setAspect: (a) => update(`set gradient aspect ${varName}`, (g) => {
187
201
  // Drop axes that equal 1 so persisted JSON stays minimal and pre-aspect
@@ -3,6 +3,7 @@
3
3
  * A component alias that owns a gradient serializes to one of two shapes:
4
4
  *
5
5
  * linear-gradient(<angle>deg, <stop>, <stop>, …)
6
+ * linear-gradient(<to side-or-corner>, <stop>, <stop>, …)
6
7
  * radial-gradient(<shape> at <cx>% 50%, <stop>, <stop>, …)
7
8
  *
8
9
  * `solid` and `none` are NOT represented here. `solid` renders as its first
@@ -35,6 +36,47 @@
35
36
  */
36
37
  import { parseColorOpacity, formatColorOpacity } from './colorOpacity';
37
38
 
39
+ /** The `to <side-or-corner>` keywords CSS accepts in place of an angle. A
40
+ * keyword angles the gradient line off the box's own diagonal, so it tracks
41
+ * the element's aspect ratio where a fixed angle cannot: `to bottom right`
42
+ * reads as ~95deg across a wide heading and ~111deg once that heading wraps.
43
+ * That is the whole reason to carry them — a degree cannot express it. */
44
+ export const LINEAR_DIRECTIONS = [
45
+ 'to top',
46
+ 'to top right',
47
+ 'to right',
48
+ 'to bottom right',
49
+ 'to bottom',
50
+ 'to bottom left',
51
+ 'to left',
52
+ 'to top left',
53
+ ] as const;
54
+
55
+ export type LinearDirection = (typeof LINEAR_DIRECTIONS)[number];
56
+
57
+ /** The angle each keyword equals on a square box. Sides are exact at any
58
+ * aspect (`to right` is 90deg, always); corners hold only at 1:1, which is
59
+ * the point of the keyword. Stored alongside a direction so the dial shows
60
+ * something honest and clearing the keyword lands on the nearest angle
61
+ * rather than snapping to 0deg (bottom-to-top). */
62
+ export const DIRECTION_ANGLES: Record<LinearDirection, number> = {
63
+ 'to top': 0,
64
+ 'to top right': 45,
65
+ 'to right': 90,
66
+ 'to bottom right': 135,
67
+ 'to bottom': 180,
68
+ 'to bottom left': 225,
69
+ 'to left': 270,
70
+ 'to top left': 315,
71
+ };
72
+
73
+ function parseDirection(raw: string): LinearDirection | null {
74
+ const norm = raw.trim().toLowerCase().replace(/\s+/g, ' ');
75
+ return (LINEAR_DIRECTIONS as readonly string[]).includes(norm)
76
+ ? (norm as LinearDirection)
77
+ : null;
78
+ }
79
+
38
80
  export interface GradientStop {
39
81
  /** 0–100 percentage along the gradient axis. */
40
82
  position: number;
@@ -54,6 +96,10 @@ export interface GradientStop {
54
96
  export interface GradientValue {
55
97
  type: 'linear' | 'radial' | 'solid' | 'none';
56
98
  angle: number;
99
+ /** Emitted in place of `angle` on a `linear` gradient. `angle` is retained
100
+ * underneath so clearing the direction restores the user's degrees, the
101
+ * same way a radial keeps its angle. */
102
+ direction?: LinearDirection;
57
103
  radius?: number;
58
104
  centerX?: number;
59
105
  aspectX?: number;
@@ -101,7 +147,8 @@ function formatRadialShape(v: GradientValue): string {
101
147
  * into a CSS background declaration.
102
148
  * - `none` → `transparent` (no background paint).
103
149
  * - `solid` → the first stop's colour (no gradient function).
104
- * - `linear` → `linear-gradient(<angle>deg, <stops>)`.
150
+ * - `linear` → `linear-gradient(<angle>deg, <stops>)`, or
151
+ * `linear-gradient(<direction>, <stops>)` when `direction` is set.
105
152
  * - `radial` → `radial-gradient(<shape> at <centerX>% 50%, <stops>)`, where
106
153
  * shape is `circle [radius]` at aspect 1 and `ellipse rx ry` otherwise.
107
154
  * Aspects are independent stretch factors, not an area-preserving ratio:
@@ -113,12 +160,16 @@ export function formatGradientValue(v: GradientValue): string {
113
160
  return first ? formatStopColor(first) : 'transparent';
114
161
  }
115
162
  const stops = formatGradientStops(v.stops);
116
- if (v.type === 'linear') return `linear-gradient(${v.angle}deg, ${stops})`;
163
+ if (v.type === 'linear') {
164
+ return `linear-gradient(${v.direction ?? `${v.angle}deg`}, ${stops})`;
165
+ }
117
166
  return `radial-gradient(${formatRadialShape(v)} at ${v.centerX ?? 50}% 50%, ${stops})`;
118
167
  }
119
168
 
120
169
  const NUM = String.raw`-?\d+(?:\.\d+)?`;
121
- const LINEAR_RE = new RegExp(String.raw`^linear-gradient\(\s*(${NUM})deg\s*,\s*(.+)\)$`, 'i');
170
+ const HEADING = String.raw`${NUM}deg|to\s+\w+(?:\s+\w+)?`;
171
+ const LINEAR_RE = new RegExp(String.raw`^linear-gradient\(\s*(${HEADING})\s*,\s*(.+)\)$`, 'i');
172
+ const DEG_RE = new RegExp(String.raw`^(${NUM})deg$`, 'i');
122
173
  const RADIAL_RE = new RegExp(
123
174
  String.raw`^radial-gradient\(\s*(circle|circle\s+${NUM}px|ellipse\s+${NUM}px\s+${NUM}px)\s+at\s+(${NUM})%\s+50%\s*,\s*(.+)\)$`,
124
175
  'i',
@@ -192,8 +243,15 @@ export function parseGradientValue(value: string): GradientValue | null {
192
243
  const css = value.trim();
193
244
  const linear = css.match(LINEAR_RE);
194
245
  if (linear) {
246
+ const deg = linear[1].match(DEG_RE);
247
+ const direction = deg ? null : parseDirection(linear[1]);
248
+ // A heading that is neither degrees nor a keyword we know is not our form.
249
+ if (!deg && !direction) return null;
195
250
  const stops = parseStops(linear[2]);
196
- return stops && { type: 'linear', angle: parseFloat(linear[1]), stops };
251
+ if (!stops) return null;
252
+ return deg
253
+ ? { type: 'linear', angle: parseFloat(deg[1]), stops }
254
+ : { type: 'linear', angle: 0, direction: direction!, stops };
197
255
  }
198
256
  const radial = css.match(RADIAL_RE);
199
257
  if (!radial) return null;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Gradients slice — fixed four-slot scale (--gradient-1 … --gradient-4),
2
+ * Gradients slice — an open `--gradient-N` library seeded with four slots,
3
3
  * each rendering to a single CSS var. Stops carry token-name references
4
4
  * (`--color-brand-500`); the renderer wraps them in `var(...)` so palette
5
5
  * edits flow through.
@@ -8,15 +8,34 @@ import type { EditorState, GradientToken, GradientTokenStop, GradientType } from
8
8
  import type { GradientDiskToken } from '../themeTypes';
9
9
  import { mutate } from '../../store/editorCore';
10
10
  import { formatGradientValue, formatGradientStops as formatStopList } from '../parsers/gradient';
11
+ import { DIRECTION_ANGLES, type LinearDirection } from '../parsers/gradient';
11
12
 
12
13
  export { formatGradientValue };
13
14
 
15
+ /** A well-formed library slot: `--gradient-` followed by a number. Anything
16
+ * else is pre-numbering data the loader and the persistence guard reject. */
17
+ export const GRADIENT_SLOT_RE = /^--gradient-\d+$/;
18
+
19
+ export function isGradientSlot(variable: string): boolean {
20
+ return GRADIENT_SLOT_RE.test(variable);
21
+ }
22
+
23
+ /** The next free slot name, so callers never collide with an existing one. */
24
+ export function nextGradientSlot(tokens: readonly GradientToken[]): string {
25
+ const used = new Set(tokens.map((t) => t.variable));
26
+ for (let n = 1; ; n++) {
27
+ const name = `--gradient-${n}`;
28
+ if (!used.has(name)) return name;
29
+ }
30
+ }
31
+
14
32
  export function makeDefaultGradients(): GradientToken[] {
15
33
  return [
16
34
  {
17
35
  variable: '--gradient-1',
18
36
  type: 'linear',
19
- angle: 90,
37
+ angle: DIRECTION_ANGLES['to right'],
38
+ direction: 'to right',
20
39
  stops: [
21
40
  { position: 0, color: '--color-brand-500' },
22
41
  { position: 100, color: '--color-accent-500' },
@@ -34,7 +53,8 @@ export function makeDefaultGradients(): GradientToken[] {
34
53
  {
35
54
  variable: '--gradient-3',
36
55
  type: 'linear',
37
- angle: 90,
56
+ angle: DIRECTION_ANGLES['to right'],
57
+ direction: 'to right',
38
58
  stops: [
39
59
  { position: 0, color: '--color-success-500' },
40
60
  { position: 100, color: '--color-info-500' },
@@ -57,20 +77,29 @@ export function makeDefaultGradients(): GradientToken[] {
57
77
  * field and scrub the rendered `--gradient-N` strings from the vars bag — they
58
78
  * are a projection kept for production CSS, never a basis. Files without the
59
79
  * field (saved before gradients round-tripped) keep the seeded defaults, which
60
- * match what those files rendered. A file whose slots don't match the fixed
61
- * four is stale-shaped and also keeps defaults, mirroring the persistence
62
- * layer's `migrateGradients` guard.
80
+ * match what those files rendered.
81
+ *
82
+ * The library is open-ended: any number of `--gradient-N` slots loads, so a
83
+ * project can carry as many as its design needs. What still fails the guard is
84
+ * a stale *shape* — a token named `--gradient-progress`, from before the slots
85
+ * were numbered — which keeps defaults, mirroring `migrateGradients`.
63
86
  */
64
87
  export function loadGradientsFromFile(
65
88
  next: EditorState,
66
89
  gradients: GradientDiskToken[] | undefined,
67
90
  rawVars: Record<string, string>,
68
91
  ): void {
69
- const expected = makeDefaultGradients().map((g) => g.variable).sort();
70
- for (const name of expected) delete rawVars[name];
71
- const have = (gradients ?? []).map((g) => g.variable).sort();
72
- const matches = have.length === expected.length && expected.every((v, i) => v === have[i]);
73
- if (matches) next.gradients.tokens = structuredClone(gradients) as GradientToken[];
92
+ for (const g of makeDefaultGradients()) {
93
+ delete rawVars[g.variable];
94
+ delete rawVars[stopsVariable(g.variable)];
95
+ }
96
+ for (const t of gradients ?? []) {
97
+ delete rawVars[t.variable];
98
+ delete rawVars[stopsVariable(t.variable)];
99
+ }
100
+ if (gradients?.length && gradients.every((g) => isGradientSlot(g.variable))) {
101
+ next.gradients.tokens = structuredClone(gradients) as GradientToken[];
102
+ }
74
103
  }
75
104
 
76
105
  /** Stops portion only — used by the palette selector to materialize a
@@ -84,6 +113,7 @@ function formatGradient(t: GradientToken): string {
84
113
  return formatGradientValue({
85
114
  type: t.type,
86
115
  angle: t.angle,
116
+ direction: t.direction,
87
117
  centerX: t.centerX,
88
118
  aspectX: t.aspectX,
89
119
  aspectY: t.aspectY,
@@ -91,9 +121,22 @@ function formatGradient(t: GradientToken): string {
91
121
  });
92
122
  }
93
123
 
124
+ /** The suffix carrying a token's stop list on its own, so a consumer can keep
125
+ * the theme's colours while supplying its own geometry:
126
+ * `linear-gradient(to top, var(--gradient-5-stops))`. The same stops then
127
+ * serve every direction a design needs without duplicating a token per angle. */
128
+ export const STOPS_VAR_SUFFIX = '-stops';
129
+
130
+ export function stopsVariable(variable: string): string {
131
+ return `${variable}${STOPS_VAR_SUFFIX}`;
132
+ }
133
+
94
134
  export function gradientsToVars(g: EditorState['gradients']): Record<string, string> {
95
135
  const out: Record<string, string> = {};
96
- for (const t of g.tokens) out[t.variable] = formatGradient(t);
136
+ for (const t of g.tokens) {
137
+ out[t.variable] = formatGradient(t);
138
+ out[stopsVariable(t.variable)] = formatStopList(t.stops);
139
+ }
97
140
  return out;
98
141
  }
99
142
 
@@ -105,13 +148,14 @@ function findGradient(s: EditorState, variable: string): GradientToken | undefin
105
148
  * Used by the editor to restore a pre-edit snapshot on Cancel. */
106
149
  export function setGradient(
107
150
  variable: string,
108
- next: { type: GradientType; angle: number; centerX?: number; aspectX?: number; aspectY?: number; stops: GradientTokenStop[] },
151
+ next: { type: GradientType; angle: number; direction?: LinearDirection; centerX?: number; aspectX?: number; aspectY?: number; stops: GradientTokenStop[] },
109
152
  ): void {
110
153
  mutate(`replace gradient ${variable}`, (s) => {
111
154
  const t = findGradient(s, variable);
112
155
  if (!t) return;
113
156
  t.type = next.type;
114
157
  t.angle = next.angle;
158
+ t.direction = next.direction;
115
159
  t.centerX = next.centerX;
116
160
  t.aspectX = next.aspectX;
117
161
  t.aspectY = next.aspectY;
@@ -126,10 +170,27 @@ export function setGradientType(variable: string, type: GradientType): void {
126
170
  });
127
171
  }
128
172
 
173
+ /** Setting degrees clears any direction keyword: the two are alternative
174
+ * headings for the same gradient, and the one you just set is the one you
175
+ * meant. The angle underneath a direction is preserved until then. */
129
176
  export function setGradientAngle(variable: string, angle: number): void {
130
177
  mutate(`set gradient angle ${variable}`, (s) => {
131
178
  const t = findGradient(s, variable);
132
- if (t) t.angle = angle;
179
+ if (!t) return;
180
+ t.angle = angle;
181
+ t.direction = undefined;
182
+ });
183
+ }
184
+
185
+ /** `undefined` drops back to the stored `angle`. Setting a keyword also moves
186
+ * `angle` to that keyword's square-box equivalent, so the dial reflects the
187
+ * gradient and clearing the keyword keeps roughly the same direction. */
188
+ export function setGradientDirection(variable: string, direction: LinearDirection | undefined): void {
189
+ mutate(`set gradient direction ${variable}`, (s) => {
190
+ const t = findGradient(s, variable);
191
+ if (!t) return;
192
+ t.direction = direction;
193
+ if (direction) t.angle = DIRECTION_ANGLES[direction];
133
194
  });
134
195
  }
135
196
 
@@ -111,7 +111,12 @@ export interface FontSource {
111
111
 
112
112
  export type SystemCascadePreset = 'system-ui-sans' | 'system-ui-serif' | 'system-ui-mono';
113
113
  export type GenericFamily = 'sans-serif' | 'serif' | 'monospace' | 'cursive' | 'fantasy';
114
- export type FontStackVariable = '--font-display' | '--font-sans' | '--font-serif' | '--font-mono';
114
+ export type FontStackVariable =
115
+ | '--font-display'
116
+ | '--font-sans'
117
+ | '--font-serif'
118
+ | '--font-mono'
119
+ | '--font-editorial';
115
120
 
116
121
  export type FontStackSlot =
117
122
  | { kind: 'project'; familyId: string }
@@ -98,6 +98,7 @@
98
98
  { path: '01-overview', title: 'Overview' },
99
99
  { path: 'getting-started', title: 'Getting started' },
100
100
  { path: 'editing-tokens', title: 'Editing tokens' },
101
+ { path: 'sketch-mode', title: 'Sketch mode' },
101
102
  { path: 'themes-workflow', title: 'Themes' },
102
103
  { path: 'where-themes-live', title: 'Where themes live' },
103
104
  { path: 'creating-components', title: 'Creating components' },
@@ -254,7 +255,7 @@
254
255
  <SideNavigation
255
256
  class="docs-sidebar"
256
257
  sections={navSections}
257
- titleLabel="Live Tokens"
258
+ titleLabel="LiveTokens"
258
259
  titleHref="#01-overview"
259
260
  currentPath={parsedHash.chapter}
260
261
  open={sidebarOpen}
@@ -283,11 +284,11 @@
283
284
  <div class="docs-main" bind:this={scrollPane}>
284
285
  <header class="docs-page-header">
285
286
  <div class="title-block">
286
- <p class="eyebrow">Live Tokens</p>
287
+ <p class="eyebrow">LiveTokens</p>
287
288
  <h1>Documentation</h1>
288
289
  <p class="lede">
289
290
  Set up a project, edit your tokens live, and ship a theme. A short
290
- guide to styling and building with Live Tokens.
291
+ guide to styling and building with LiveTokens.
291
292
  </p>
292
293
  </div>
293
294
  </header>
@@ -13,6 +13,7 @@ export const chapters: Chapter[] = [
13
13
  { id: '01-overview', title: 'Overview' },
14
14
  { id: 'getting-started', title: 'Getting started' },
15
15
  { id: 'editing-tokens', title: 'Editing tokens' },
16
+ { id: 'sketch-mode', title: 'Sketch mode' },
16
17
  { id: 'themes-workflow', title: 'Themes' },
17
18
  { id: 'where-themes-live', title: 'Where themes live' },
18
19
  { id: 'creating-components', title: 'Creating components' },
@@ -1,6 +1,6 @@
1
1
  # Overview
2
2
 
3
- Live Tokens is a design system for building Svelte microsites quickly. You
3
+ LiveTokens is a design system for building Svelte microsites quickly. You
4
4
  style your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.
5
5
 
6
6
  ## How it works
@@ -27,6 +27,7 @@ style your site by editing tokens and components in a live editor. When it looks
27
27
  - **[Getting started](getting-started.md)**: scaffold a project and make your
28
28
  first edit.
29
29
  - **[Editing tokens](editing-tokens.md)**: a tour of the editor.
30
+ - **[Sketch mode](sketch-mode.md)**: redraw the page by hand.
30
31
  - **[Themes](themes-workflow.md)**: save, switch, and ship.
31
32
  - **[Creating components](creating-components.md)**: make your own components
32
33
  editable.
@@ -3,12 +3,16 @@
3
3
  A tour of the editor. The page behind it repaints on every change; saving
4
4
  writes a theme file you can reload later.
5
5
 
6
- The editor has two views:
6
+ The editor has four views:
7
7
 
8
8
  - **Tokens**: the design-system primitives (colour, type, spacing, and so on).
9
9
  They apply everywhere your site uses them.
10
+ - **Colors**: the harmony wheel, the palette curves, and the story your colours
11
+ tell across a page.
10
12
  - **Components**: per-component editors. Re-Assign what tokens a component uses
11
13
  without changing the underlying system.
14
+ - **Sketch**: an effect layer that redraws the page by hand. See
15
+ [Sketch mode](sketch-mode.md).
12
16
 
13
17
  This page covers **Tokens**. For components, see
14
18
  [Creating components](creating-components.md).
@@ -0,0 +1,89 @@
1
+ # Sketch mode
2
+
3
+ Sketch mode redraws your whole page as if it had been drawn by hand. Every
4
+ component keeps its own colours, spacing and corners; what changes is the line
5
+ they are drawn with.
6
+
7
+ It is an effect layer, not a set of token values. It reads nothing from your
8
+ theme and writes nothing back, so it never touches a token, never lands in a
9
+ theme file, and never reaches the CSS you ship. Turn it off and every trace of
10
+ it goes.
11
+
12
+ Open the **Sketch** view in the editor and switch **Sketch mode** on. The effect
13
+ applies to the page behind the editor as well as to the preview, so what you see
14
+ in context is what it does.
15
+
16
+ ## What it draws
17
+
18
+ Each component's fill and outline are repainted from the tokens that component
19
+ already owns. The real background and border are hidden behind them, then both
20
+ are pushed around one shared field of noise. Because every component samples the
21
+ same field, the whole page reads as one drawing rather than as a set of
22
+ separately wobbled boxes.
23
+
24
+ ## The presets
25
+
26
+ Seven looks ship with the package, and each is a complete set of dials rather
27
+ than a style name:
28
+
29
+ - **Pencil.** Two graphite passes on their own seeds, so the outline disagrees
30
+ with itself the way a hand coming back round does.
31
+ - **Marker.** A broad translucent nib gone round twice on the same line, so the
32
+ overlap darkens and the ink pools where it slows.
33
+ - **Whiteboard.** The fattest nib on glass, with a mask that streaks the fill
34
+ like a half-wiped board.
35
+ - **Hatched.** An etching. The fill is angled shading and the outline a single
36
+ hard-edged scratch.
37
+ - **Dashed.** A drafting outline: one slow drift along the ruler, broken into
38
+ strokes. The clean pole.
39
+ - **Napkin.** Ballpoint in a hurry. Everything loose at once.
40
+ - **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.
41
+
42
+ Pick one, then move whatever you like. **Save** keeps your dials under a name of
43
+ your own, alongside the shipped seven, as a file under
44
+ `src/live-tokens/data/sketch-presets/`.
45
+
46
+ ## The dials
47
+
48
+ - **Border.** How far the outline travels and how long its wave is, then its
49
+ width, ink, pressure and pooling. A second pass either copies the first line a
50
+ few pixels off or runs it through the pen again on its own seed.
51
+ - **Fill.** Solid or hatched, how far the fill's edge travels, and how far each
52
+ instance is offset, rotated and scaled from its neighbours. **Ink coverage**
53
+ thins the fill with a field of blotches: set their size, how many levels of
54
+ detail, how pale and how dense they go, and how soft their edges are.
55
+ - **Shape.** **Corner spread** rounds each corner by its own share of the dial,
56
+ so no two match. **Corner travel** leans the drawn box into a quadrilateral
57
+ with no two sides parallel. This is the dial that stops a component reading as
58
+ a rectangle.
59
+ - **Icons and SVG.** Glyph travel and wavelength on their own scale. A glyph is
60
+ all curves already, so it needs more travel than a card's long straight edge
61
+ before the wobble reads at all.
62
+ - **Noise.** The shared field itself: its wavelength, how many layers of detail
63
+ sit on it, and the shape of its wave. A square wave sends nearly every edge to
64
+ full travel, which is what makes the effect stronger rather than bigger.
65
+
66
+ ## Drawing your own elements
67
+
68
+ The effect displaces boxes, so anything drawn some other way is left alone. A
69
+ rule made from a `border` is not a box; make it an element and tell the layer
70
+ what to draw it with:
71
+
72
+ ```css
73
+ .rule {
74
+ height: var(--border-width-2);
75
+ background: var(--border-brand);
76
+ --sketch-fill: var(--border-brand);
77
+ }
78
+ ```
79
+
80
+ `--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color` and `--sketch-radius`
81
+ name the fill, the outline, the hatching ink and the corners for one element and
82
+ everything inside it. Since the layer blanks the real background, an element
83
+ whose fill matters under Sketch mode should name it here as well as paint it.
84
+
85
+ Icons and inline SVG take the wobble directly, since a glyph has no box to
86
+ redraw. Body type is left alone: an icon is a shape and survives a wobble, a
87
+ paragraph is not. To keep the icons in a subtree crisp, set
88
+ `--sketch-icon-off: none` on its root. It inherits, so one declaration covers
89
+ everything under it.
@@ -2,10 +2,11 @@
2
2
  // Source of truth: src/editor/docs/content/*.md · regenerate: npm run sync:docs
3
3
 
4
4
  export const docContent: Record<string, string> = {
5
- "01-overview": "# Overview\n\nLive Tokens 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- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
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
6
  "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
7
- "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has two views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** 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",
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- **Colors**: 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- **Sketch**: 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
+ "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 reads nothing from your\ntheme and writes nothing back, so it never touches a token, never lands in a\ntheme file, and never reaches the CSS you ship. Turn it off and every trace of\nit goes.\n\nOpen the **Sketch** 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 presets\n\nSeven looks ship with the package, and each is a complete set of dials rather\nthan a style 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** keeps your dials under a name of\nyour own, alongside the shipped seven, as a file under\n`src/live-tokens/data/sketch-presets/`.\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, how many levels of\n detail, how pale and how dense they go, and how soft their edges are.\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## Drawing your own elements\n\nThe effect displaces boxes, so anything drawn some other way is left alone. A\nrule made from a `border` is not a box; make it an element and tell the layer\nwhat to draw it with:\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\n`--sketch-fill`, `--sketch-stroke`, `--sketch-hatch-color` and `--sketch-radius`\nname the fill, the outline, the hatching ink and the corners for one element and\neverything inside it. Since the layer blanks the real background, an element\nwhose fill matters under Sketch mode should name it here as well as paint it.\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. To keep the icons in a subtree crisp, set\n`--sketch-icon-off: none` on its root. It inherits, so one declaration covers\neverything under it.\n",
9
10
  "themes-workflow": "# Themes\n\nSave your work, switch between looks, and ship one to production.\n\n## The Theme panel\n\nThe **Theme** panel at the foot of the editor sidebar holds the whole look:\ncolors, type, and a setting for every component, in one file. It carries the\nname the look ships under, whether production is running it, and **Adopt**.\nTwo parts sit under it, each a read-out rather than a file to manage.\n\n- **Colors & Type** holds the design tokens. Components read those tokens to\n define their appearance. It names the two faces the page is showing.\n- **Components** counts how many components have an unsaved edit that has not\n been saved into the theme, and opens the component editors.\n\nA theme holds its own copy of every part, so one theme can never break another.\n\n## How themes work\n\nA theme is a document, and the editor works the way any editor does.\n\n- **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries\n the whole look: the colors and type plus a setting for every component.\n- **The open theme** is the one the editor is working on, named in\n `themes/_active.json`. One at a time.\n- **Your unsaved edits** are what the page shows right now. The editor keeps\n them in your browser as you work and writes them to a buffer, `_working.json`,\n one slot per part of the look. **Save** captures that buffer into the open\n theme.\n- **The production theme** is the one your site ships, named in\n `themes/_production.json`. **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, so you can\ntry each look and compare. **Save** in that window opens and adopts the previewed\ntheme in one step: the active pointer changes, the buffers clear, the editor\nworks on it, and production ships it. **Cancel** returns you to where you were.\nPreviewing alone never changes what your site ships.\n\n**Colors and type only. Keep my shapes.** narrows the load to the palette and\nthe fonts: your component settings stay as they are and the theme you have open\nstays open. Saved colors and type files are listed there too, marked *colors &\ntype*, and picking one is always that narrower load.\n\n## Shipping\n\n**Adopt**, in the Theme panel, is the \"ship it\" step, and it ships the whole\nlook. It saves the open theme, then bakes that theme into\n`src/live-tokens/data/tokens.generated.css`, which your build bundles alongside\n`tokens.css`: the colors and type plus every component the theme carries. Fonts\nregenerate to match. The line under the theme name says whether production is\nrunning this theme.\n\nProduction is one saved theme, so nothing else publishes. Trying a look, moving\na token, saving a theme: all of it leaves the generated CSS alone until you\nAdopt. A component editor's Adopt runs the same whole-look step, because a\ncomponent never ships alone. Adopting while Motion Proto is open saves your look\nas a theme of your own first, since the built-in one is read-only.\n\nProduction builds (`npm run build`) ship only that plain CSS and your\ncomponents. No editor, no JSON loading, no runtime indirection.\n\n## Keeping your work safe\n\nEverything under `src/live-tokens/data/` is plain JSON, so commit it. Themes show\nup as readable diffs you can review per branch, and the buffer shows up as the\nwork you have not saved into a theme yet. Nothing is backed up anywhere else:\ngit is your safety net. To experiment freely, **Save As** a new name first, then\nedit.\n\n## Where to go next\n\n- **[Where themes live](where-themes-live.md)**: the files behind all of this,\n and what writes each one.\n- **[Creating components](creating-components.md)**: make your own components\n editable in the same editor.\n",
10
11
  "where-themes-live": "# Where themes live\n\nEverything the editor writes is plain JSON and CSS inside your project. There\nis no database and no hidden state: the files are the storage, and git is the\nhistory.\n\n## The data tree\n\n```\nsrc/live-tokens/data/\n themes/\n _active.json names the theme the editor has open\n _production.json names the theme your site ships\n default.json Motion Proto, the built-in look, rewritten at boot\n my-brand.json a saved theme: the whole look in one file\n colors-and-type/\n _working.json unsaved colors and type edits\n component-configs/\n button/\n default.json Button's shipped settings, derived at boot\n _working.json unsaved Button edits\n my-button.json a preset you saved from the Button editor\n tokens.generated.css the baked CSS your production build ships\nsrc/system/styles/\n tokens.css your token vocabulary, hand-authored, never written\n fonts.css font imports, rewritten when you Adopt\n```\n\nA saved theme carries the whole look by value: the colors and type plus a\nsetting for every component. It depends on no other file, so deleting\nanything 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. That file is the\n durable copy of your look; matching buffers are then removed.\n- **Load** clears the buffers and points `themes/_active.json` at the theme you\n picked. Live reads fall through to that file. Nothing else changes, so trying\n looks is free and ordinary switching changes only the pointer.\n- **Adopt** points `themes/_production.json` at the open theme, bakes it into\n `tokens.generated.css`, and rewrites `fonts.css` to match. It is the only\n action that changes what your site ships.\n\nThe `default.json` files are the shipped baseline. The editor derives them at\nboot and refreshes them when the package updates; it never saves your work\nover them.\n\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",
11
12
  };
@@ -472,6 +472,21 @@
472
472
  border-radius var(--bar-open-dur) var(--bar-open-ease) var(--bar-open-delay);
473
473
  }
474
474
 
475
+ /* Sketch mode scopes itself to the host document root, and this bar lives
476
+ there too. The effect is for the page being designed, not for the tool
477
+ looking at it. */
478
+ .lt-overlay {
479
+ --sketch-icon-off: none;
480
+ }
481
+
482
+ /* tokens.css sets the theme font on :where(*), and a matching rule beats
483
+ inheritance, so the panel's own font-family never reaches its children.
484
+ Restore inheritance for the chrome, as .editor-page does. The preview
485
+ lives in an iframe, so no themed surface is caught by this. */
486
+ .lt-overlay :global(*:not([class*='fa-'])) {
487
+ font-family: inherit;
488
+ }
489
+
475
490
  .lt-overlay.docked {
476
491
  border-right: none;
477
492
  border-radius: 0;