@motion-proto/live-tokens 0.61.0 → 0.63.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 (83) hide show
  1. package/.claude/skills/live-tokens-adjust-geometry/SKILL.md +4 -4
  2. package/.claude/skills/live-tokens-build-page/SKILL.md +21 -3
  3. package/.claude/skills/live-tokens-create-component/SKILL.md +16 -48
  4. package/.claude/skills/live-tokens-create-component/references/fixed-overlays.md +10 -1
  5. package/.claude/skills/live-tokens-create-component/references/intrinsics.md +7 -5
  6. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +1 -1
  7. package/.claude/skills/live-tokens-create-component/references/token-naming.md +50 -0
  8. package/.claude/skills/live-tokens-generate-theme/SKILL.md +16 -9
  9. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +8 -6
  10. package/.claude/skills/live-tokens-pick-component/SKILL.md +19 -5
  11. package/CHANGELOG.md +114 -0
  12. package/bin/migrate.mjs +6 -2
  13. package/dist-plugin/adjust/index.cjs +1 -1
  14. package/dist-plugin/adjust/index.d.cts +1 -1
  15. package/dist-plugin/adjust/index.d.ts +1 -1
  16. package/dist-plugin/adjust/index.js +1 -1
  17. package/dist-plugin/{chunk-232GZGQU.js → chunk-NDJJORKJ.js} +342 -5
  18. package/dist-plugin/{chunk-Y5CNFSSV.js → chunk-RVE3MNKM.js} +1 -1
  19. package/dist-plugin/{chunk-OIOXU7FR.js → chunk-ZHPX7ZYQ.js} +83 -25
  20. package/dist-plugin/{dataPaths-CRfD1LdA.d.ts → dataPaths-DZUzVv8H.d.cts} +3 -3
  21. package/dist-plugin/{dataPaths-CRfD1LdA.d.cts → dataPaths-DZUzVv8H.d.ts} +3 -3
  22. package/dist-plugin/fontPairing/index.cjs +1 -1
  23. package/dist-plugin/fontPairing/index.d.cts +1 -1
  24. package/dist-plugin/fontPairing/index.d.ts +1 -1
  25. package/dist-plugin/fontPairing/index.js +1 -1
  26. package/dist-plugin/generateColorsAndType/index.cjs +1 -1
  27. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  28. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  29. package/dist-plugin/generateColorsAndType/index.js +1 -1
  30. package/dist-plugin/index.cjs +461 -58
  31. package/dist-plugin/index.d.cts +1 -1
  32. package/dist-plugin/index.d.ts +1 -1
  33. package/dist-plugin/index.js +31 -23
  34. package/dist-plugin/migrateData/index.cjs +350 -9
  35. package/dist-plugin/migrateData/index.d.cts +1 -1
  36. package/dist-plugin/migrateData/index.d.ts +1 -1
  37. package/dist-plugin/migrateData/index.js +9 -5
  38. package/dist-plugin/tokensCssMigrations/index.cjs +83 -25
  39. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  40. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  41. package/dist-plugin/tokensCssMigrations/index.js +2 -2
  42. package/package.json +4 -2
  43. package/src/app/site.css +32 -0
  44. package/src/editor/component-editor/CardEditor.svelte +7 -1
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +25 -2
  46. package/src/editor/core/preview/lookPreview.ts +46 -5
  47. package/src/editor/core/productionPulse.ts +6 -2
  48. package/src/editor/core/sketch/maskField.ts +75 -39
  49. package/src/editor/core/sketch/sketchLayer.ts +32 -11
  50. package/src/editor/core/sketch/sketchStore.ts +311 -86
  51. package/src/editor/core/sketch/{sketchPresetService.ts → sketchStyleService.ts} +14 -14
  52. package/src/editor/core/sketch/{sketchPresets.ts → sketchStyles.ts} +27 -28
  53. package/src/editor/core/themes/themeDocumentSync.ts +2 -0
  54. package/src/editor/core/themes/themeInit.ts +19 -1
  55. package/src/editor/core/themes/themeService.ts +7 -2
  56. package/src/editor/core/themes/themeTypes.ts +5 -0
  57. package/src/editor/docs/content/editing-tokens.md +1 -1
  58. package/src/editor/docs/content/sketch-mode.md +34 -18
  59. package/src/editor/docs/content/themes-workflow.md +30 -21
  60. package/src/editor/docs/content/where-themes-live.md +9 -5
  61. package/src/editor/docs/content.generated.ts +4 -4
  62. package/src/editor/overlay/LiveTokensRouter.svelte +8 -0
  63. package/src/editor/ui/EditorViewSwitcher.svelte +3 -3
  64. package/src/editor/ui/ThemePanel.svelte +47 -1
  65. package/src/editor/ui/sections/textStyles.ts +29 -1
  66. package/src/editor/ui/sketch/SketchPreview.svelte +3 -3
  67. package/src/editor/ui/sketch/SketchTab.svelte +120 -134
  68. package/src/live-tokens/data/colors-and-type/midnight-study.json +40 -30
  69. package/src/live-tokens/data/themes/autumn.json +3 -3
  70. package/src/live-tokens/data/themes/halloween.json +3 -3
  71. package/src/live-tokens/data/themes/midnight-study.json +61 -49
  72. package/src/live-tokens/data/themes/ocean.json +3 -3
  73. package/src/live-tokens/data/themes/royal-velvet.json +3 -3
  74. package/src/live-tokens/data/themes/sketchy.json +3 -3
  75. package/src/live-tokens/data/themes/spring-meadow.json +3 -3
  76. package/src/live-tokens/data/themes/sunset.json +3 -3
  77. package/src/system/components/Card.svelte +27 -9
  78. package/src/system/components/FloatingTokenTags.css +10 -8
  79. package/src/system/components/ImageLightbox.svelte +5 -2
  80. package/src/system/components/SectionDivider.svelte +3 -3
  81. package/src/system/components/SegmentedControl.svelte +11 -9
  82. package/src/system/styles/tokens.css +28 -5
  83. package/template/src/pages/Home.svelte +1 -11
@@ -1,24 +1,24 @@
1
1
  import { API_BASE } from '../storage/apiBase';
2
- import { hydrateSketchSettings, type SketchSettings } from './sketchPresets';
2
+ import { hydrateSketchStyle, type SketchStyle } from './sketchStyles';
3
3
 
4
- export interface SketchPresetFile {
4
+ export interface SketchStyleFile {
5
5
  name: string;
6
6
  createdAt?: string;
7
7
  updatedAt?: string;
8
- settings: SketchSettings;
8
+ settings: SketchStyle;
9
9
  }
10
10
 
11
- export interface SketchPresetMeta {
11
+ export interface SketchStyleMeta {
12
12
  name: string;
13
13
  fileName: string;
14
14
  updatedAt: string;
15
15
  }
16
16
 
17
- const BASE = `${API_BASE}/sketch-presets`;
17
+ const BASE = `${API_BASE}/sketch-styles`;
18
18
 
19
19
  /** Slug for the file name. The display name is stored inside the file, so this
20
20
  only has to be stable, unique-ish and inside the route's charset. */
21
- export function slugifySketchPreset(name: string): string {
21
+ export function slugifySketchStyle(name: string): string {
22
22
  return name
23
23
  .toLowerCase()
24
24
  .replace(/[^a-z0-9]+/g, '-')
@@ -26,24 +26,24 @@ export function slugifySketchPreset(name: string): string {
26
26
  .slice(0, 60);
27
27
  }
28
28
 
29
- export async function listSketchPresets(): Promise<SketchPresetMeta[]> {
29
+ export async function listSketchStyles(): Promise<SketchStyleMeta[]> {
30
30
  const res = await fetch(BASE);
31
- if (!res.ok) throw new Error('Failed to list sketch presets');
31
+ if (!res.ok) throw new Error('Failed to list sketchstyles');
32
32
  const body = await res.json();
33
33
  return body.files ?? [];
34
34
  }
35
35
 
36
- export async function loadSketchPreset(fileName: string): Promise<SketchPresetFile> {
36
+ export async function loadSketchStyle(fileName: string): Promise<SketchStyleFile> {
37
37
  const res = await fetch(`${BASE}/${encodeURIComponent(fileName)}`);
38
- if (!res.ok) throw new Error(`Failed to load sketch preset: ${fileName}`);
38
+ if (!res.ok) throw new Error(`Failed to load sketchstyle: ${fileName}`);
39
39
  const body = await res.json();
40
- return { ...body, settings: hydrateSketchSettings(body.settings) };
40
+ return { ...body, settings: hydrateSketchStyle(body.settings) };
41
41
  }
42
42
 
43
- export async function saveSketchPreset(
43
+ export async function saveSketchStyle(
44
44
  fileName: string,
45
45
  name: string,
46
- settings: SketchSettings,
46
+ settings: SketchStyle,
47
47
  ): Promise<void> {
48
48
  const res = await fetch(`${BASE}/${encodeURIComponent(fileName)}`, {
49
49
  method: 'PUT',
@@ -56,7 +56,7 @@ export async function saveSketchPreset(
56
56
  }
57
57
  }
58
58
 
59
- export async function deleteSketchPreset(fileName: string): Promise<void> {
59
+ export async function deleteSketchStyle(fileName: string): Promise<void> {
60
60
  const res = await fetch(`${BASE}/${encodeURIComponent(fileName)}`, { method: 'DELETE' });
61
61
  if (!res.ok) {
62
62
  const err = await res.json().catch(() => ({ error: 'Delete failed' }));
@@ -1,4 +1,4 @@
1
- export interface SketchSettings {
1
+ export interface SketchStyle {
2
2
  label: string;
3
3
  blurb: string;
4
4
  /** How far the fill's edge travels at its furthest, in px. Every dial that
@@ -62,11 +62,10 @@ export interface SketchSettings {
62
62
  /** Wavelength of the coverage noise, in page px. Small gives speckle, large gives broad patches. */
63
63
  maskBlob: number;
64
64
  /** Output levels on the coverage field, 0 to 1: the palest the fill gets and
65
- the densest. 0 is bare and 1 is whole, so 0.4 to 1 is a fill that is never
66
- thinner than 40% ink, and 0 to 0.8 one that never quite fills in. Close
67
- together is a flat wash, far apart a strong blotch. The field is stretched
68
- onto its own measured range before they apply, so both mean the same thing
69
- at every grain and octave count. */
65
+ the densest. The whole field is squeezed into the gap, never cut at it, so
66
+ 0.4 to 1 is a fill that is never thinner than 40% ink and 0 to 0.8 one
67
+ that never quite fills in. Close together is a flat wash, far apart a
68
+ strong blotch. */
70
69
  maskOutputMin: number;
71
70
  maskOutputMax: number;
72
71
  /** Detail layers. 1-2 gives broad blobs, 4+ goes cloudy and stops reading as blotches. */
@@ -120,7 +119,7 @@ export interface SketchSettings {
120
119
  iconMaskScale: number;
121
120
  }
122
121
 
123
- const base: SketchSettings = {
122
+ const base: SketchStyle = {
124
123
  label: '', blurb: '',
125
124
  fillTravel: 1.5, strokeTravel: 1.5, wobble: 45, roughness: 3,
126
125
  borderWavelength: 1, waveform: 1,
@@ -134,13 +133,13 @@ const base: SketchSettings = {
134
133
  iconTravel: 1.25, iconWavelength: 0.625, iconMaskOn: true, iconMaskScale: 1,
135
134
  };
136
135
 
137
- export const SKETCH_PRESETS: Record<string, SketchSettings> = {
136
+ export const SKETCH_STYLES: Record<string, SketchStyle> = {
138
137
  pencil: {
139
138
  ...base, label: 'Pencil',
140
139
  blurb: 'Two graphite passes on their own seeds, so the outline disagrees with itself the way a hand coming back round does. Tight grain, little else.',
141
140
  fillTravel: 0.75, strokeTravel: 1.25, wobble: 30, roughness: 3, waveform: 1,
142
141
  strokeWidth: 1.25, doubleStroke: true, retracePass: 'reseeded', retraceOffset: 1.5, strokeInk: 0.85,
143
- maskBlob: 40, maskOutputMin: 0.62, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 1, maskSoftness: 0.6,
142
+ maskBlob: 40, maskOutputMin: 0.62, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 1, maskSoftness: 0,
144
143
  jitterX: 1.5, jitterY: 1.5, jitterRot: 0.35, jitterScale: 0.022,
145
144
  cornerSpread: 6, cornerTravel: 4.5,
146
145
  pressure: 0.15, pressureMod: 0.3, pooling: 0, iconTravel: 0.75,
@@ -151,7 +150,7 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
151
150
  blurb: 'Broad translucent nib gone round twice on the same line, so the overlap darkens and the ink pools where it slows.',
152
151
  fillTravel: 2, strokeTravel: 1.5, wobble: 56, waveform: 1.4, borderWavelength: 1.3,
153
152
  strokeWidth: 4, doubleStroke: true, retracePass: 'copy', strokeInk: 0.52, retraceOffset: 2.2,
154
- maskBlob: 115, maskOutputMin: 0.21, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 4, maskSoftness: 2.5,
153
+ maskBlob: 115, maskOutputMin: 0.42, maskOutputMax: 1, maskOctaves: 3, maskPosterize: 4, maskSoftness: 4,
155
154
  jitterX: 3, jitterY: 3, jitterRot: 0.8, jitterScale: 0.045,
156
155
  cornerSpread: 10, cornerTravel: 8,
157
156
  pressure: 0.2, pressureMod: 0.25, pooling: 2, iconTravel: 1.25, iconMaskScale: 4,
@@ -162,8 +161,8 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
162
161
  blurb: 'The fattest nib on glass. One long smooth undulation, and a veined mask that streaks the fill like a half-wiped board.',
163
162
  fillTravel: 2.5, strokeTravel: 2.5, wobble: 90, roughness: 1, waveform: 1, borderWavelength: 1.5,
164
163
  strokeWidth: 5.5, doubleStroke: true, retracePass: 'copy', strokeInk: 0.66, retraceOffset: 3,
165
- maskGrain: 'turbulence', maskBlob: 180, maskOutputMin: 0.42, maskOutputMax: 0.9,
166
- maskOctaves: 1, maskPosterize: 3, maskSoftness: 5,
164
+ maskGrain: 'turbulence', maskBlob: 180, maskOutputMin: 0.42, maskOutputMax: 1,
165
+ maskOctaves: 1, maskPosterize: 3, maskSoftness: 8,
167
166
  jitterX: 4.5, jitterY: 4.5, jitterRot: 1.2, jitterScale: 0.07,
168
167
  cornerSpread: 14, cornerTravel: 11,
169
168
  pressure: 0.15, pressureMod: 0.15, pooling: 3.5, iconTravel: 1.75, iconMaskScale: 1.5,
@@ -196,7 +195,7 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
196
195
  blurb: 'Ballpoint in a hurry. Everything loose at once: a square wave sends every edge to full travel, and the second pass lands wherever it lands.',
197
196
  fillTravel: 3, strokeTravel: 2.25, wobble: 50, roughness: 3, waveform: 2.5,
198
197
  strokeWidth: 2.25, doubleStroke: true, retracePass: 'reseeded', retraceOffset: 4, strokeInk: 1,
199
- maskBlob: 150, maskOutputMin: 0.43, maskOutputMax: 0.95, maskOctaves: 2, maskPosterize: 2, maskSoftness: 6.7,
198
+ maskBlob: 150, maskOutputMin: 0.43, maskOutputMax: 0.95, maskOctaves: 2, maskPosterize: 2, maskSoftness: 10,
200
199
  jitterX: 6, jitterY: 6, jitterRot: 1.8, jitterScale: 0.1,
201
200
  cornerSpread: 20, cornerTravel: 17,
202
201
  pressure: 0.45, pressureMod: 0.6, pooling: 2.5, iconTravel: 2.25, iconMaskScale: 3.6,
@@ -207,7 +206,7 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
207
206
  blurb: 'Ink that ran out. One scratchy pass that breaks up along its length, over a fill the mask has worn nearly through in patches.',
208
207
  fillTravel: 2.25, strokeTravel: 1.75, wobble: 50, waveform: 2, borderWavelength: 0.5,
209
208
  strokeWidth: 3.5, doubleStroke: false, strokeInk: 0.4,
210
- maskBlob: 150, maskOutputMin: 0.15, maskOutputMax: 0.91,
209
+ maskBlob: 150, maskOutputMin: 0.24, maskOutputMax: 0.91,
211
210
  maskOctaves: 2, maskPosterize: 4, maskSoftness: 1.75,
212
211
  jitterX: 5, jitterY: 5, jitterRot: 1.4, jitterScale: 0.08,
213
212
  cornerSpread: 16, cornerTravel: 13,
@@ -215,17 +214,17 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
215
214
  },
216
215
  };
217
216
 
218
- export const DEFAULT_SKETCH_PRESET = 'marker';
217
+ export const DEFAULT_SKETCH_STYLE = 'marker';
219
218
 
220
- /** Reconciled against a full preset in both directions: a value stored before a
219
+ /** Reconciled against a full sketchstyle in both directions: a value stored before a
221
220
  control existed picks up the default, and a value stored for a control since
222
221
  retired is dropped. Without the drop, a stale key survives every spread and
223
222
  makes the settings compare unequal to any baseline forever. */
224
- export function hydrateSketchSettings(raw: unknown): SketchSettings {
225
- const base = SKETCH_PRESETS[DEFAULT_SKETCH_PRESET];
226
- const stored = (raw ?? {}) as Partial<SketchSettings>;
223
+ export function hydrateSketchStyle(raw: unknown): SketchStyle {
224
+ const base = SKETCH_STYLES[DEFAULT_SKETCH_STYLE];
225
+ const stored = (raw ?? {}) as Partial<SketchStyle>;
227
226
  const out = { ...base };
228
- for (const key of Object.keys(base) as (keyof SketchSettings)[]) {
227
+ for (const key of Object.keys(base) as (keyof SketchStyle)[]) {
229
228
  if (stored[key] !== undefined) (out[key] as unknown) = stored[key];
230
229
  }
231
230
  // A retired option: the fill's presence belongs to the theme, not the effect.
@@ -242,8 +241,8 @@ export function hydrateSketchSettings(raw: unknown): SketchSettings {
242
241
 
243
242
  /** The second pass used to sit at a distance derived from the stroke width,
244
243
  with no dial of its own. A look stored before the dial comes back at that
245
- distance rather than at whatever the fallback preset happens to carry. */
246
- function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchSettings): void {
244
+ distance rather than at whatever the fallback sketchstyle happens to carry. */
245
+ function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyle): void {
247
246
  if (stored.retraceOffset === undefined) {
248
247
  out.retraceOffset = Number(Math.max(1.2, out.strokeWidth * 0.55).toFixed(2));
249
248
  }
@@ -263,7 +262,7 @@ const SWING_DIALS = {
263
262
  /** The pen wobble used to be stated as `frequency`, in cycles per px, which is
264
263
  the number the filter wants and not one anybody can picture. It is a
265
264
  wavelength in px now, the way the mask states its blobs. */
266
- function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchSettings): void {
265
+ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchStyle): void {
267
266
  const cycles = stored.frequency;
268
267
  if (typeof cycles === 'number' && cycles > 0) out.wobble = Math.round(1 / cycles);
269
268
  // The layer count was `octaves`, and its dial ran to 5. The top two moved
@@ -286,13 +285,13 @@ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchS
286
285
  of the field, so it came out either untouched or gone. It is a share of the
287
286
  glyph now, and the old default reads as one period across the glyph, which
288
287
  is what that size was aiming at. */
289
- function convertIconTileToScale(stored: Record<string, unknown>, out: SketchSettings): void {
288
+ function convertIconTileToScale(stored: Record<string, unknown>, out: SketchStyle): void {
290
289
  const tile = stored.iconMaskTile;
291
290
  if (typeof tile !== 'number') return;
292
291
  out.iconMaskScale = Math.min(5, Math.max(0.5, Number((tile / 90).toFixed(2))));
293
292
  }
294
293
 
295
- function halveSwingDials(stored: Record<string, unknown>, out: SketchSettings): void {
294
+ function halveSwingDials(stored: Record<string, unknown>, out: SketchStyle): void {
296
295
  for (const [legacy, key] of Object.entries(SWING_DIALS)) {
297
296
  const value = stored[legacy];
298
297
  if (typeof value === 'number') out[key as 'fillTravel'] = value / 2;
@@ -302,7 +301,7 @@ function halveSwingDials(stored: Record<string, unknown>, out: SketchSettings):
302
301
  /** The mask used to be a 600-unit tile painted at `maskScale` px, with the
303
302
  coverage point buried in the hardness slope. Recover page-px blobs and
304
303
  softness, and the levels the old slope and floor put the edge at. */
305
- function convertTiledMask(stored: Record<string, unknown>, out: SketchSettings): void {
304
+ function convertTiledMask(stored: Record<string, unknown>, out: SketchStyle): void {
306
305
  const scale = stored.maskScale;
307
306
  if (typeof scale !== 'number') return;
308
307
  const freq = stored.maskFrequency;
@@ -317,7 +316,7 @@ function convertTiledMask(stored: Record<string, unknown>, out: SketchSettings):
317
316
  * levels the pair put the edge between, and rescale them onto the field as it
318
317
  * is now: stretched onto its full range before the levels see it.
319
318
  */
320
- function convertCutToLevels(stored: Record<string, unknown>, out: SketchSettings): void {
319
+ function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyle): void {
321
320
  const contrast = stored.maskContrast;
322
321
  const coverage = stored.maskCoverage;
323
322
  if (typeof contrast !== 'number' || typeof coverage !== 'number') return;
@@ -333,7 +332,7 @@ function convertCutToLevels(stored: Record<string, unknown>, out: SketchSettings
333
332
  now, the palest and densest the fill gets. The same two numbers carry over:
334
333
  a look cut between 20% and 50% comes back as a wash between 20% and 50% ink,
335
334
  which keeps its spread and loses only the hole. */
336
- function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchSettings): void {
335
+ function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchStyle): void {
337
336
  if (typeof stored.maskLevelMin === 'number') out.maskOutputMin = stored.maskLevelMin;
338
337
  if (typeof stored.maskLevelMax === 'number') out.maskOutputMax = stored.maskLevelMax;
339
338
  }
@@ -2,6 +2,7 @@ import { liveMovedSinceBake } from '../productionPulse';
2
2
  import { openThemeSlug } from '../store/editorConfigStore';
3
3
  import { loadThemeFromApi } from '../store/editorStore';
4
4
  import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
5
+ import { openThemeSketchStyle } from '../sketch/sketchStore';
5
6
  import type { ApplyThemeResult } from './themeService';
6
7
 
7
8
  const CHANNEL_NAME = 'live-tokens:active-theme:v1';
@@ -42,6 +43,7 @@ export function hydrateAppliedTheme(fileName: string, result: ApplyThemeResult):
42
43
  migrateColorsAndTypeFonts(colorsAndType);
43
44
  loadThemeFromApi(colorsAndType, structuredClone(result.componentConfigs));
44
45
  openThemeSlug.set(fileName);
46
+ openThemeSketchStyle(result.theme.sketchStyle);
45
47
  liveMovedSinceBake.set(false);
46
48
  if (typeof document !== 'undefined') {
47
49
  document.dispatchEvent(new CustomEvent<AppliedThemeDetail>(THEME_APPLIED_EVENT, {
@@ -1,10 +1,11 @@
1
- import type { AliasDiskValue, ColorsAndType } from './themeTypes';
1
+ import type { AliasDiskValue, ColorsAndType, Theme } from './themeTypes';
2
2
  import { openThemeSlug } from '../store/editorConfigStore';
3
3
  import { migrateColorsAndTypeFonts } from '../fonts/fontMigration';
4
4
  import { loadFromFile, seedComponentsFromApi } from '../store/editorStore';
5
5
  import { getActiveComponentConfig, type ComponentSummary } from '../components/componentConfigService';
6
6
  import { safeFetch } from '../storage/storage';
7
7
  import { API_BASE } from '../storage/apiBase';
8
+ import { hasPersistedSketchState, openThemeSketchStyle, themeSketchStyle } from '../sketch/sketchStore';
8
9
 
9
10
  interface ListComponentsDto {
10
11
  components: ComponentSummary[];
@@ -61,4 +62,21 @@ export async function initializeTheme(): Promise<void> {
61
62
  // and CSS defaults because one request happened to fail during boot.
62
63
  if (!componentReadFailed) seedComponentsFromApi(configs);
63
64
  }
65
+
66
+ const active = await safeFetch<Theme>(`${API_BASE}/themes/active`);
67
+ // A failed fetch is not "the theme carries no sketchstyle": treating null as
68
+ // absent would tell the panel the look is off the theme, or hand a fresh
69
+ // browser a blank buffer, over a fetch that will likely succeed next time.
70
+ if (active) {
71
+ if (hasPersistedSketchState()) {
72
+ // The buffer already painted on the first frame; boot only learns what
73
+ // the theme holds so unsaved dial work reads as unsaved rather than
74
+ // getting silently overwritten (that overwrite is what Apply is for).
75
+ themeSketchStyle.set(active.sketchStyle);
76
+ } else {
77
+ // Nothing was ever recorded in this browser: the theme's value becomes
78
+ // the live value, the same reconciliation opening a theme performs.
79
+ openThemeSketchStyle(active.sketchStyle);
80
+ }
81
+ }
64
82
  }
@@ -7,6 +7,7 @@ import { getActiveColorsAndType } from './colorsAndTypeService';
7
7
  import { broadcastAppliedTheme, hydrateAppliedTheme } from './themeDocumentSync';
8
8
  import { CURRENT_COMPONENT_SCHEMA_VERSION } from './migrations';
9
9
  import { THEME_SCHEMA_VERSION } from './themeTypes';
10
+ import { liveSketchStyle, themeSketchStyle } from '../sketch/sketchStore';
10
11
 
11
12
  export type { ThemeFillReport };
12
13
 
@@ -144,7 +145,7 @@ function withoutLiveMarkers<T extends { _fileName?: string; _source?: unknown }>
144
145
  * way out of `GET /colors-and-type/active`, which matters: the server trusts an
145
146
  * already-embedded copy and runs no migrations over it on write.
146
147
  */
147
- async function captureLook(): Promise<Pick<Theme, 'colorsAndType' | 'componentConfigs'>> {
148
+ async function captureLook(): Promise<Pick<Theme, 'colorsAndType' | 'componentConfigs' | 'sketchStyle'>> {
148
149
  const liveColorsAndType = await getActiveColorsAndType();
149
150
  if (!liveColorsAndType) {
150
151
  throw new Error('No live colors and type to capture');
@@ -156,7 +157,9 @@ async function captureLook(): Promise<Pick<Theme, 'colorsAndType' | 'componentCo
156
157
  const config = configs[i];
157
158
  if (config) componentConfigs[c.name] = withoutLiveMarkers(config);
158
159
  });
159
- return { colorsAndType: withoutLiveMarkers(liveColorsAndType), componentConfigs };
160
+ // Sketchstyle has no server door of its own (RJC 8): the live buffer, not a
161
+ // fetch, is the source of truth for what the dials currently say.
162
+ return { colorsAndType: withoutLiveMarkers(liveColorsAndType), componentConfigs, sketchStyle: liveSketchStyle() };
160
163
  }
161
164
 
162
165
  /**
@@ -178,6 +181,7 @@ export async function saveAsTheme(
178
181
  componentSchemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
179
182
  ...look,
180
183
  });
184
+ themeSketchStyle.set(look.sketchStyle);
181
185
  liveMovedSinceBake.set(true);
182
186
  await setActiveTheme(fileName);
183
187
  }
@@ -201,6 +205,7 @@ export async function saveActiveTheme(displayName?: string): Promise<void> {
201
205
  componentSchemaVersion: CURRENT_COMPONENT_SCHEMA_VERSION,
202
206
  ...look,
203
207
  });
208
+ themeSketchStyle.set(look.sketchStyle);
204
209
  liveMovedSinceBake.set(true);
205
210
  }
206
211
 
@@ -2,6 +2,7 @@ import type { CurveAnchor } from '../../ui/curveEngine';
2
2
  import type { GradientValue } from './parsers/gradient';
3
3
  import type { Oklch } from '../palettes/oklch';
4
4
  import type { HarmonyAxis } from '../palettes/colorHarmony';
5
+ import type { SketchStyle } from '../sketch/sketchStyles';
5
6
  /** Single source of truth for the theme schema version
6
7
  * (docs/plans/theme-completeness.md, Wave 2 step 5). It lives here, on the
7
8
  * shipped side: `vite-plugin/` is build tooling and is not in the tarball, so
@@ -279,6 +280,10 @@ export interface Theme {
279
280
  * buy no isolation. `normalizeTheme` sets this on every read; an embedded
280
281
  * config's own `schemaVersion` (if hand-authored) is ignored and stripped. */
281
282
  componentSchemaVersion: number;
283
+ /** The sketchstyle this look paints, by value. Absent means the look is
284
+ * crisp: presence is the on state, so there is no separate flag that can
285
+ * disagree with the dials beside it (RJC 1). */
286
+ sketchStyle?: SketchStyle;
282
287
  /** Server-attached file-name marker. Same role as `ColorsAndType._fileName`. */
283
288
  _fileName?: string;
284
289
  }
@@ -11,7 +11,7 @@ The editor has four views:
11
11
  tell across a page.
12
12
  - **Components**: per-component editors. Re-Assign what tokens a component uses
13
13
  without changing the underlying system.
14
- - **Sketch Style**: an effect layer that redraws the page by hand. See
14
+ - **Sketchstyle**: an effect layer that redraws the page by hand. See
15
15
  [Sketch mode](sketch-mode.md).
16
16
 
17
17
  This page covers **Tokens**. For components, see
@@ -4,12 +4,11 @@ Sketch mode redraws your whole page as if it had been drawn by hand. Every
4
4
  component keeps its own colours, spacing and corners; what changes is the line
5
5
  they are drawn with.
6
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.
7
+ It is an effect layer, not a set of token values. It never touches a token
8
+ itself, so turning it off returns every component to exactly what its tokens
9
+ already say. A production build never carries the drawing.
11
10
 
12
- Open the **Sketch Style** view in the editor and switch **Sketch mode** on. The effect
11
+ Open the **Sketchstyle** view in the editor and switch **Sketch mode** on. The effect
13
12
  applies to the page behind the editor as well as to the preview, so what you see
14
13
  in context is what it does.
15
14
 
@@ -21,10 +20,10 @@ are pushed around one shared field of noise. Because every component samples the
21
20
  same field, the whole page reads as one drawing rather than as a set of
22
21
  separately wobbled boxes.
23
22
 
24
- ## The presets
23
+ ## The sketchstyles
25
24
 
26
25
  Seven looks ship with the package, and each is a complete set of dials rather
27
- than a style name:
26
+ than just a name:
28
27
 
29
28
  - **Pencil.** Two graphite passes on their own seeds, so the outline disagrees
30
29
  with itself the way a hand coming back round does.
@@ -39,9 +38,10 @@ than a style name:
39
38
  - **Napkin.** Ballpoint in a hurry. Everything loose at once.
40
39
  - **Dry marker.** Ink that ran out. One scratchy pass over a mostly eaten fill.
41
40
 
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/`.
41
+ Pick one, then move whatever you like. **Save as sketchstyle…** keeps your
42
+ dials under a name of your own, alongside the shipped seven, as a file under
43
+ `src/live-tokens/data/sketch-styles/`. That is a different gesture from
44
+ saving a theme; see "Where the settings live" below.
45
45
 
46
46
  ## The dials
47
47
 
@@ -50,8 +50,13 @@ your own, alongside the shipped seven, as a file under
50
50
  few pixels off or runs it through the pen again on its own seed.
51
51
  - **Fill.** Solid or hatched, how far the fill's edge travels, and how far each
52
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.
53
+ thins the fill with a field of blotches: set their size and how many levels of
54
+ detail, then work the field as a levels control. The field always runs black
55
+ to white whatever the noise underneath. Steps flattens it into tones, and
56
+ Output squeezes the whole of it into the range the ink covers, from how pale
57
+ it gets at its thinnest to how dense at its fullest. Menus and tooltips are
58
+ drawn solid whatever the coverage dials say: they float over the page, and a
59
+ fill worn through in patches lets the page show through them.
55
60
  - **Shape.** **Corner spread** rounds each corner by its own share of the dial,
56
61
  so no two match. **Corner travel** leans the drawn box into a quadrilateral
57
62
  with no two sides parallel. This is the dial that stops a component reading as
@@ -65,14 +70,25 @@ your own, alongside the shipped seven, as a file under
65
70
 
66
71
  ## Where the settings live
67
72
 
68
- The dials you are moving live in your browser, so the effect follows you across
69
- reloads and stays off everyone else's screen. **Save** writes a named preset to
70
- `src/live-tokens/data/sketch-presets/`, which is the only thing that reaches
71
- disk.
73
+ The sketch layer is part of the theme, the same way colors and type are.
74
+ **Save** in the Theme panel folds your dials into the open theme; **Load**
75
+ applies whatever a theme carries, and turns the effect off for a theme that
76
+ carries none.
77
+
78
+ Until you save, the dials sit in your browser only. The Theme panel calls
79
+ that state off the theme, the same word it uses for an unsaved component
80
+ change. The built-in **Motion Proto** theme is read-only, so Save
81
+ is disabled there; use **Save As** to fold the dials into a theme of your
82
+ own.
83
+
84
+ **Save as sketchstyle…** in the **Sketchstyle** view is a different gesture. It
85
+ writes a named sketchstyle to `src/live-tokens/data/sketch-styles/`, a look you
86
+ can pick from any theme. It never touches the open theme, and it never marks
87
+ the look off the theme.
72
88
 
73
89
  Sketch mode is a tool for looking at the page, not a layer the page can ship.
74
- Nothing is written into a theme, `tokens.generated.css` never sees it, and a
75
- production build has no sketch layer in it at all.
90
+ The theme records the dials, but nothing bakes them: `tokens.generated.css`
91
+ never sees them, and a production build has no sketch layer in it at all.
76
92
 
77
93
  ## Drawing your own elements
78
94
 
@@ -5,14 +5,19 @@ Save your work, switch between looks, and ship one to production.
5
5
  ## The Theme panel
6
6
 
7
7
  The **Theme** panel at the foot of the editor sidebar holds the whole look:
8
- colors, type, and a setting for every component, in one file. It carries the
9
- name the look ships under, whether production is running it, and **Adopt**.
10
- Two parts sit under it, each a read-out rather than a file to manage.
8
+ colors, type, a setting for every component, and the sketch layer, in one
9
+ file. It carries the name the look ships under, whether production is running
10
+ it, and **Adopt**. Three parts sit under it, each a read-out rather than a
11
+ file to manage.
11
12
 
12
13
  - **Colors & Type** holds the design tokens. Components read those tokens to
13
14
  define their appearance. It names the two faces the page is showing.
14
15
  - **Components** counts how many components have an unsaved edit that has not
15
16
  been saved into the theme, and opens the component editors.
17
+ - **Sketchstyle** names the look the theme's sketch layer carries: its label, or
18
+ off the theme when what's on screen no longer matches what was saved, or
19
+ none when the theme carries no sketch layer. It travels with the theme
20
+ like colors and type do, but never reaches a production build.
16
21
 
17
22
  A theme holds its own copy of every part, so one theme can never break another.
18
23
 
@@ -21,13 +26,15 @@ A theme holds its own copy of every part, so one theme can never break another.
21
26
  A theme is a document, and the editor works the way any editor does.
22
27
 
23
28
  - **A theme** is a named JSON file in `src/live-tokens/data/themes/`. It carries
24
- the whole look: the colors and type plus a setting for every component.
29
+ the whole look: the colors and type, a setting for every component, and the
30
+ sketch layer.
25
31
  - **The open theme** is the one the editor is working on, named in
26
32
  `themes/_active.json`. One at a time.
27
33
  - **Your unsaved edits** are what the page shows right now. The editor keeps
28
- them in your browser as you work and writes them to a buffer, `_working.json`,
29
- one slot per part of the look. **Save** captures that buffer into the open
30
- theme.
34
+ them in your browser as you work, writing most parts to a buffer,
35
+ `_working.json`, one slot each; the sketch layer has no buffer and stays
36
+ live in the browser until you save. **Save** captures all of it into the
37
+ open theme.
31
38
  - **The production theme** is the one your site ships, named in
32
39
  `themes/_production.json`. **Adopt** changes it; saving a preset in the Theme
33
40
  Picker performs that Adopt for you.
@@ -96,29 +103,31 @@ to it, and the editor never overwrites it, so start your own with **Save As**.
96
103
  ## Switching
97
104
 
98
105
  **Load**—or clicking the active theme's name—opens the Theme Picker. Picking a
99
- theme shows it on the page as a preview with nothing written to disk, so you can
100
- try each look and compare. **Save** in that window opens and adopts the previewed
101
- theme in one step: the active pointer changes, the buffers clear, the editor
102
- works on it, and production ships it. **Cancel** returns you to where you were.
103
- Previewing alone never changes what your site ships.
106
+ theme shows it on the page as a preview with nothing written to disk, sketch
107
+ layer included, so you can try each look and compare. **Save** in that window
108
+ opens and adopts the previewed theme in one step: the active pointer changes,
109
+ the buffers clear, the editor works on it, and production ships it. **Cancel**
110
+ returns you to where you were, unsaved sketch dials included. Previewing alone
111
+ never changes what your site ships.
104
112
 
105
113
  **Colors and type only. Keep my shapes.** narrows the load to the palette and
106
- the fonts: your component settings stay as they are and the theme you have open
107
- stays open. Saved colors and type files are listed there too, marked *colors &
108
- type*, and picking one is always that narrower load.
114
+ the fonts: your component settings and your sketch layer stay as they are, and
115
+ the theme you have open stays open. Saved colors and type files are listed
116
+ there too, marked *colors & type*, and picking one is always that narrower
117
+ load.
109
118
 
110
119
  ## Shipping
111
120
 
112
- **Adopt**, in the Theme panel, is the "ship it" step, and it ships the whole
113
- look. It saves the open theme, then bakes that theme into
121
+ **Adopt**, in the Theme panel, is the "ship it" step. It saves the open theme,
122
+ then bakes the colors and type plus every component the theme carries into
114
123
  `src/live-tokens/data/tokens.generated.css`, which your build bundles alongside
115
- `tokens.css`: the colors and type plus every component the theme carries. Fonts
116
- regenerate to match. The line under the theme name says whether production is
117
- running this theme.
124
+ `tokens.css`. The sketch layer is the one part of the theme Adopt never bakes:
125
+ it stays a preview. Fonts regenerate to match. The line under the theme name
126
+ says whether production is running this theme.
118
127
 
119
128
  Production is one saved theme, so nothing else publishes. Trying a look, moving
120
129
  a token, saving a theme: all of it leaves the generated CSS alone until you
121
- Adopt. A component editor's Adopt runs the same whole-look step, because a
130
+ Adopt. A component editor's Adopt runs the same save-then-bake step, because a
122
131
  component never ships alone. Adopting while Motion Proto is open saves your look
123
132
  as a theme of your own first, since the built-in one is read-only.
124
133
 
@@ -20,15 +20,17 @@ src/live-tokens/data/
20
20
  default.json Button's shipped settings, derived at boot
21
21
  _working.json unsaved Button edits
22
22
  my-button.json a preset you saved from the Button editor
23
+ sketch-styles/
24
+ my-look.json a sketchstyle saved from the Sketchstyle view
23
25
  tokens.generated.css the baked CSS your production build ships
24
26
  src/system/styles/
25
27
  tokens.css your token vocabulary, hand-authored, never written
26
28
  fonts.css font imports, rewritten when you Adopt
27
29
  ```
28
30
 
29
- A saved theme carries the whole look by value: the colors and type plus a
30
- setting for every component. It depends on no other file, so deleting
31
- anything else never breaks it.
31
+ A saved theme carries the whole look by value: the colors and type, a
32
+ setting for every component, and the sketch layer. It depends on no other
33
+ file, so deleting anything else never breaks it.
32
34
 
33
35
  ## What writes when
34
36
 
@@ -36,8 +38,10 @@ anything else never breaks it.
36
38
  edits in the browser as you work and writes them to the `_working.json`
37
39
  buffers when you save a component. When the Theme panel finds several dirty
38
40
  components, **Save all** writes those buffers together.
39
- - **Save** captures the buffers into the open theme's file. That file is the
40
- durable copy of your look; matching buffers are then removed.
41
+ - **Save** captures the buffers into the open theme's file, along with the
42
+ sketch layer, which has no buffer of its own and lives only in the browser
43
+ until Save writes it. That file is the durable copy of your look; matching
44
+ buffers are then removed.
41
45
  - **Load** clears the buffers and points `themes/_active.json` at the theme you
42
46
  picked. Live reads fall through to that file. Nothing else changes, so trying
43
47
  looks is free and ordinary switching changes only the pointer.