@motion-proto/live-tokens 0.62.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 (67) 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 +41 -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-OPYOK2CA.js → chunk-ZHPX7ZYQ.js} +39 -1
  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 +417 -34
  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 +39 -1
  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 +16 -0
  44. package/src/editor/core/preview/lookPreview.ts +46 -5
  45. package/src/editor/core/productionPulse.ts +6 -2
  46. package/src/editor/core/sketch/maskField.ts +5 -5
  47. package/src/editor/core/sketch/sketchLayer.ts +8 -8
  48. package/src/editor/core/sketch/sketchStore.ts +233 -79
  49. package/src/editor/core/sketch/{sketchPresetService.ts → sketchStyleService.ts} +14 -14
  50. package/src/editor/core/sketch/{sketchPresets.ts → sketchStyles.ts} +17 -17
  51. package/src/editor/core/themes/themeDocumentSync.ts +2 -0
  52. package/src/editor/core/themes/themeInit.ts +19 -1
  53. package/src/editor/core/themes/themeService.ts +7 -2
  54. package/src/editor/core/themes/themeTypes.ts +5 -0
  55. package/src/editor/docs/content/editing-tokens.md +1 -1
  56. package/src/editor/docs/content/sketch-mode.md +27 -16
  57. package/src/editor/docs/content/themes-workflow.md +30 -21
  58. package/src/editor/docs/content/where-themes-live.md +9 -5
  59. package/src/editor/docs/content.generated.ts +4 -4
  60. package/src/editor/overlay/LiveTokensRouter.svelte +1 -1
  61. package/src/editor/ui/EditorViewSwitcher.svelte +3 -3
  62. package/src/editor/ui/ThemePanel.svelte +47 -1
  63. package/src/editor/ui/sections/textStyles.ts +15 -1
  64. package/src/editor/ui/sketch/SketchPreview.svelte +3 -3
  65. package/src/editor/ui/sketch/SketchTab.svelte +87 -53
  66. package/src/system/components/FloatingTokenTags.css +10 -8
  67. package/src/system/styles/tokens.css +17 -2
@@ -1,24 +1,71 @@
1
1
  import { derived, get, writable } from 'svelte/store';
2
2
  import {
3
- SKETCH_PRESETS,
4
- DEFAULT_SKETCH_PRESET,
5
- hydrateSketchSettings,
6
- type SketchSettings,
7
- } from './sketchPresets';
3
+ SKETCH_STYLES,
4
+ DEFAULT_SKETCH_STYLE,
5
+ hydrateSketchStyle,
6
+ type SketchStyle,
7
+ } from './sketchStyles';
8
8
  import { applySketchLayer, hostRoot, removeSketchLayer, setSketchScope } from './sketchLayer';
9
+ import { liveMovedSinceBake } from '../productionPulse';
9
10
  import {
10
- deleteSketchPreset,
11
- listSketchPresets,
12
- loadSketchPreset,
13
- saveSketchPreset,
14
- slugifySketchPreset,
15
- type SketchPresetMeta,
16
- } from './sketchPresetService';
11
+ deleteSketchStyle,
12
+ listSketchStyles,
13
+ loadSketchStyle,
14
+ saveSketchStyle,
15
+ slugifySketchStyle,
16
+ type SketchStyleMeta,
17
+ } from './sketchStyleService';
17
18
 
18
19
  const ENABLED_KEY = 'lt.sketchEnabled';
19
20
  const SETTINGS_KEY = 'lt.sketchSettings';
20
- const PRESET_KEY = 'lt.sketchPreset';
21
+ const STYLE_NAME_KEY = 'lt.sketchStyleName';
21
22
  const BASELINE_KEY = 'lt.sketchBaseline';
23
+ /** Set only by a genuine user decision (`markSketchTouched`), never by the
24
+ `subscribe` write-back below. That write-back plants the four keys above
25
+ on every boot regardless of whether a dial was ever moved, so deriving
26
+ "touched" from their presence would answer "yes" from the second load
27
+ onward — this key is the thing that actually distinguishes the two. */
28
+ const TOUCHED_KEY = 'lt.sketchTouched';
29
+
30
+ /** True once this browser has recorded a genuine sketch decision, so boot
31
+ (`themeInit.ts`) can tell "never touched" from "explicitly off" and only
32
+ seed the live buffer from the theme in the first case. Mutated in place
33
+ by `markSketchTouched`; the first read migrates a browser that already
34
+ held real dial state before this key existed (RJC 7) so it starts
35
+ touched, not one this module is about to seed for the first time. */
36
+ let sketchTouched = (() => {
37
+ try {
38
+ const recorded = localStorage.getItem(TOUCHED_KEY);
39
+ if (recorded !== null) return recorded === 'true';
40
+ const priorUse = localStorage.getItem(ENABLED_KEY) !== null || localStorage.getItem(SETTINGS_KEY) !== null;
41
+ localStorage.setItem(TOUCHED_KEY, String(priorUse));
42
+ return priorUse;
43
+ } catch {
44
+ return false;
45
+ }
46
+ })();
47
+
48
+ /** Reads `TOUCHED_KEY` fresh rather than the cached `sketchTouched`, because a
49
+ peer document's write lands in localStorage between this module's import
50
+ and a caller's await (`themeInit.ts`'s boot reconcile is exactly that
51
+ gap): the cache would still answer as it did at import time. */
52
+ export function hasPersistedSketchState(): boolean {
53
+ try {
54
+ return localStorage.getItem(TOUCHED_KEY) === 'true';
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ /** Called from every control a user can act on: a dial, a sketchstyle pick, the
61
+ on/off switch. Never from `openThemeSketchStyle` or the storage-sync `adopt`
62
+ path, both of which write the same four keys as a side effect of state
63
+ this browser did not decide on its own. */
64
+ function markSketchTouched(): void {
65
+ if (sketchTouched) return;
66
+ sketchTouched = true;
67
+ persist(TOUCHED_KEY, 'true');
68
+ }
22
69
 
23
70
  function readEnabled(): boolean {
24
71
  try {
@@ -28,61 +75,61 @@ function readEnabled(): boolean {
28
75
  }
29
76
  }
30
77
 
31
- function readSettings(): SketchSettings {
78
+ function readSettings(): SketchStyle {
32
79
  try {
33
80
  const raw = localStorage.getItem(SETTINGS_KEY);
34
- if (raw) return hydrateSketchSettings(JSON.parse(raw));
81
+ if (raw) return hydrateSketchStyle(JSON.parse(raw));
35
82
  } catch {
36
- // fall through to the default preset
83
+ // fall through to the default sketchstyle
37
84
  }
38
- return { ...SKETCH_PRESETS[DEFAULT_SKETCH_PRESET] };
85
+ return { ...SKETCH_STYLES[DEFAULT_SKETCH_STYLE] };
39
86
  }
40
87
 
41
- function readBaseline(): SketchSettings | null {
88
+ function readBaseline(): SketchStyle | null {
42
89
  try {
43
90
  const raw = localStorage.getItem(BASELINE_KEY);
44
- if (raw) return hydrateSketchSettings(JSON.parse(raw));
91
+ if (raw) return hydrateSketchStyle(JSON.parse(raw));
45
92
  } catch {
46
93
  // fall through
47
94
  }
48
95
  return null;
49
96
  }
50
97
 
51
- /** Marks a saved preset in `sketchPreset`, so a user file named `pencil` and
52
- the shipped `pencil` stay distinguishable in one string. */
53
- export const USER_PRESET_PREFIX = 'user:';
98
+ /** Marks a saved sketchstyle in `sketchStyleName`, so a user file named `pencil`
99
+ and the shipped `pencil` stay distinguishable in one string. */
100
+ export const USER_STYLE_PREFIX = 'user:';
54
101
 
55
- function readPresetName(): string {
102
+ function readStyleName(): string {
56
103
  try {
57
- const name = localStorage.getItem(PRESET_KEY);
58
- if (name === '' || (name && (name in SKETCH_PRESETS || name.startsWith(USER_PRESET_PREFIX)))) {
104
+ const name = localStorage.getItem(STYLE_NAME_KEY);
105
+ if (name === '' || (name && (name in SKETCH_STYLES || name.startsWith(USER_STYLE_PREFIX)))) {
59
106
  return name;
60
107
  }
61
108
  } catch {
62
109
  // fall through
63
110
  }
64
- return DEFAULT_SKETCH_PRESET;
111
+ return DEFAULT_SKETCH_STYLE;
65
112
  }
66
113
 
67
- /** Off by default: the effect is a draft look, never something a project
68
- inherits without asking for it. */
114
+ /** Off by default: the effect paints nothing until the open theme's own
115
+ sketchstyle, or a decision this browser already recorded, says otherwise. */
69
116
  export const sketchEnabled = writable<boolean>(readEnabled());
70
- export const sketchSettings = writable<SketchSettings>(readSettings());
71
- /** The preset the dials started from. It survives dial moves, so the grid keeps
72
- showing what the current look is closest to; empty only when nothing was
73
- picked, or the picked file was deleted. */
74
- export const sketchPreset = writable<string>(readPresetName());
117
+ export const sketchSettings = writable<SketchStyle>(readSettings());
118
+ /** The sketchstyle the dials started from. It survives dial moves, so the grid
119
+ keeps showing what the current look is closest to; empty only when nothing
120
+ was picked, or the picked file was deleted. */
121
+ export const sketchStyleName = writable<string>(readStyleName());
75
122
 
76
- /** The settings as the selected preset defined them. Kept beside the live
123
+ /** The settings as the selected sketchstyle defined them. Kept beside the live
77
124
  settings so "modified" is derived by comparison rather than tracked as a
78
125
  flag: dial a value back to where it started and the tab stops claiming a
79
126
  change, with no bookkeeping to get wrong. */
80
- export const sketchBaseline = writable<SketchSettings | null>(readBaseline());
127
+ export const sketchBaseline = writable<SketchStyle | null>(readBaseline());
81
128
 
82
- /** Dial-set fields only. `label` and `blurb` name the preset rather than
129
+ /** Dial-set fields only. `label` and `blurb` name the sketchstyle rather than
83
130
  describe the look, and no dial writes them. */
84
- function sameLook(a: SketchSettings, b: SketchSettings): boolean {
85
- return (Object.keys(a) as (keyof SketchSettings)[])
131
+ function sameLook(a: SketchStyle, b: SketchStyle): boolean {
132
+ return (Object.keys(a) as (keyof SketchStyle)[])
86
133
  .filter((k) => k !== 'label' && k !== 'blurb')
87
134
  .every((k) => a[k] === b[k]);
88
135
  }
@@ -94,60 +141,122 @@ export const sketchDirty = derived(
94
141
 
95
142
  export const sketchBlurb = derived(sketchSettings, (s) => s.blurb);
96
143
 
97
- export function selectSketchPreset(name: string): void {
98
- const preset = SKETCH_PRESETS[name];
99
- if (!preset) return;
100
- sketchPreset.set(name);
101
- sketchBaseline.set({ ...preset });
102
- sketchSettings.set({ ...preset });
144
+ /** The live look as a theme would carry it: the dials when the effect is
145
+ on, nothing when it is off (RJC 1). A copy, not the store's own object,
146
+ so a caller holding onto the result can never observe a later dial move
147
+ through it. */
148
+ export function liveSketchStyle(): SketchStyle | undefined {
149
+ return get(sketchEnabled) ? { ...get(sketchSettings) } : undefined;
150
+ }
151
+
152
+ /** The Sketchstyle view's on/off switch goes through here rather than a bare
153
+ `sketchEnabled.set`, so flipping it by hand marks this browser as having
154
+ made a sketch decision the same as any other control does. */
155
+ export function setSketchEnabled(enabled: boolean): void {
156
+ markSketchTouched();
157
+ if (enabled !== get(sketchEnabled)) liveMovedSinceBake.set(true);
158
+ sketchEnabled.set(enabled);
159
+ }
160
+
161
+ /** What the open theme holds, so "unsaved" is a comparison rather than a
162
+ flag (RJC 5). Set by every path that opens or saves a theme. */
163
+ export const themeSketchStyle = writable<SketchStyle | undefined>(undefined);
164
+
165
+ /** Open a theme's sketchstyle: the dials, the on/off state, and the name
166
+ recovered by comparison (RJC 3). Overwrites the live buffer, which
167
+ is what opening a theme means everywhere else (RJC 6). Never writes a
168
+ `user:` name here: a saved sketchstyle is a file this look may not have come
169
+ from, and the only thing that can name one is picking it. */
170
+ export function openThemeSketchStyle(sketchStyle: SketchStyle | undefined): void {
171
+ themeSketchStyle.set(sketchStyle);
172
+ if (!sketchStyle) {
173
+ sketchEnabled.set(false);
174
+ sketchBaseline.set(null);
175
+ sketchStyleName.set('');
176
+ return;
177
+ }
178
+ const matched = (Object.keys(SKETCH_STYLES) as string[]).find((name) => sameLook(SKETCH_STYLES[name], sketchStyle));
179
+ sketchSettings.set({ ...sketchStyle });
180
+ sketchBaseline.set({ ...sketchStyle });
181
+ sketchStyleName.set(matched ?? '');
182
+ sketchEnabled.set(true);
103
183
  }
104
184
 
105
- /** Saved presets, listed from the data tree. Empty until `refreshUserPresets`
106
- runs, so importing this module never reaches for the network. */
107
- export const userSketchPresets = writable<SketchPresetMeta[]>([]);
185
+ /** The live sketch differs from what the open theme carries. Presence is
186
+ half the comparison: on with dials the theme does not hold, or off while
187
+ the theme holds a layer, are both off the theme. */
188
+ export const sketchOffLook = derived(
189
+ [sketchEnabled, sketchSettings, themeSketchStyle],
190
+ ([enabled, settings, saved]) => {
191
+ const live = enabled ? settings : undefined;
192
+ if (!live && !saved) return false;
193
+ if (!live || !saved) return true;
194
+ return !sameLook(live, saved);
195
+ },
196
+ );
108
197
 
109
- export async function refreshUserPresets(): Promise<void> {
110
- userSketchPresets.set(await listSketchPresets());
198
+ export function selectSketchStyle(name: string): void {
199
+ const style = SKETCH_STYLES[name];
200
+ if (!style) return;
201
+ markSketchTouched();
202
+ if (get(sketchEnabled)) liveMovedSinceBake.set(true);
203
+ sketchStyleName.set(name);
204
+ sketchBaseline.set({ ...style });
205
+ sketchSettings.set({ ...style });
111
206
  }
112
207
 
113
- export async function selectUserSketchPreset(fileName: string): Promise<void> {
114
- const file = await loadSketchPreset(fileName);
115
- sketchPreset.set(USER_PRESET_PREFIX + fileName);
208
+ /** Saved sketchstyles, listed from the data tree. Empty until
209
+ `refreshSavedSketchStyles` runs, so importing this module never reaches for
210
+ the network. */
211
+ export const savedSketchStyles = writable<SketchStyleMeta[]>([]);
212
+
213
+ export async function refreshSavedSketchStyles(): Promise<void> {
214
+ savedSketchStyles.set(await listSketchStyles());
215
+ }
216
+
217
+ export async function selectSavedSketchStyle(fileName: string): Promise<void> {
218
+ const file = await loadSketchStyle(fileName);
219
+ markSketchTouched();
220
+ if (get(sketchEnabled)) liveMovedSinceBake.set(true);
221
+ sketchStyleName.set(USER_STYLE_PREFIX + fileName);
116
222
  sketchBaseline.set({ ...file.settings });
117
223
  sketchSettings.set(file.settings);
118
224
  }
119
225
 
120
226
  /** Writes whatever the dials currently say to a named file and selects it, so
121
227
  saving leaves the tab pointing at the thing that was just saved rather than
122
- back on "Adjusted from a preset". Returns the file name it allocated. */
123
- export async function saveCurrentAsSketchPreset(name: string): Promise<string> {
228
+ back on "Adjusted from a sketchstyle". Returns the file name it allocated. */
229
+ export async function saveCurrentSketchStyle(name: string): Promise<string> {
124
230
  const trimmed = name.trim();
125
- if (!trimmed) throw new Error('A preset needs a name');
126
- const fileName = slugifySketchPreset(trimmed);
231
+ if (!trimmed) throw new Error('A sketchstyle needs a name');
232
+ const fileName = slugifySketchStyle(trimmed);
127
233
  if (!fileName) throw new Error('That name has no letters or digits to make a file name from');
234
+ markSketchTouched();
128
235
  const settings = { ...get(sketchSettings), label: trimmed };
129
- await saveSketchPreset(fileName, trimmed, settings);
130
- await refreshUserPresets();
236
+ await saveSketchStyle(fileName, trimmed, settings);
237
+ await refreshSavedSketchStyles();
131
238
  sketchSettings.set(settings);
132
239
  sketchBaseline.set({ ...settings });
133
- sketchPreset.set(USER_PRESET_PREFIX + fileName);
240
+ sketchStyleName.set(USER_STYLE_PREFIX + fileName);
134
241
  return fileName;
135
242
  }
136
243
 
137
- export async function deleteUserSketchPreset(fileName: string): Promise<void> {
138
- await deleteSketchPreset(fileName);
139
- await refreshUserPresets();
140
- // The dials keep their values; only the label stops naming a file that exists.
141
- if (get(sketchPreset) === USER_PRESET_PREFIX + fileName) {
142
- sketchPreset.set('');
244
+ export async function deleteSavedSketchStyle(fileName: string): Promise<void> {
245
+ await deleteSketchStyle(fileName);
246
+ await refreshSavedSketchStyles();
247
+ // The dials keep their values; only the name stops naming a file that exists.
248
+ if (get(sketchStyleName) === USER_STYLE_PREFIX + fileName) {
249
+ sketchStyleName.set('');
143
250
  sketchBaseline.set(null);
144
251
  }
145
252
  }
146
253
 
147
254
  /** Every dial goes through here. The selection deliberately survives: the grid
148
- keeps naming the preset this look came from, and `sketchDirty` reports the
149
- drift. Save writes a new preset rather than overwriting the base. */
150
- export function updateSketchSettings(patch: Partial<SketchSettings>): void {
255
+ keeps naming the sketchstyle this look came from, and `sketchDirty` reports
256
+ the drift. Save writes a new sketchstyle rather than overwriting the base. */
257
+ export function updateSketchSettings(patch: Partial<SketchStyle>): void {
258
+ markSketchTouched();
259
+ if (get(sketchEnabled)) liveMovedSinceBake.set(true);
151
260
  sketchSettings.update((s) => ({ ...s, ...patch }));
152
261
  }
153
262
 
@@ -155,7 +264,7 @@ function persist(key: string, value: string): void {
155
264
  try {
156
265
  localStorage.setItem(key, value);
157
266
  } catch {
158
- // Private-mode storage denial is not worth surfacing for a draft look.
267
+ // Private-mode storage denial is not worth surfacing for one browser's dials.
159
268
  }
160
269
  }
161
270
 
@@ -169,7 +278,7 @@ let pageRoot: HTMLElement | null = null;
169
278
 
170
279
  let installed = false;
171
280
 
172
- function render(enabled: boolean, settings: SketchSettings): void {
281
+ function render(enabled: boolean, settings: SketchStyle): void {
173
282
  if (typeof document === 'undefined') return;
174
283
  if (!enabled) {
175
284
  if (installed) removeSketchLayer();
@@ -185,6 +294,50 @@ function render(enabled: boolean, settings: SketchSettings): void {
185
294
  setSketchScope(pageRoot, settings);
186
295
  }
187
296
 
297
+ /** Paint one look on the two roots `render` owns, and on nothing else.
298
+
299
+ `render(false, ...)` reaches further: it clears every `[data-sketch]` in
300
+ the document. The scopes it takes with it belong to `$effect`s that paint
301
+ from the live stores, which a preview deliberately never ticks, so those
302
+ never come back. Previewing a theme carrying no sketchstyle left the
303
+ Sketchstyle view's own stage crisp for good, Cancel included. */
304
+ function paintPreviewRoots(style: SketchStyle | undefined): void {
305
+ if (typeof document === 'undefined') return;
306
+ if (style) {
307
+ applySketchLayer(style);
308
+ installed = true;
309
+ }
310
+ setSketchScope(hostRoot(), style ?? null);
311
+ setSketchScope(pageRoot, style ?? null);
312
+ }
313
+
314
+ /** Paint a sketchstyle for the Theme Picker preview, bypassing the live
315
+ buffer entirely: going through `sketchSettings`/`sketchEnabled` would
316
+ `share()` the previewed dials into localStorage and overwrite whatever
317
+ the user had live, which is right for Apply (RJC 6) but would destroy
318
+ unsaved work the moment the picker opened a row. The paint reaches the
319
+ host page across the iframe boundary the same way Apply's does; only the
320
+ store write and the persistence are skipped. Pass `undefined` for a theme
321
+ that carries no sketchstyle. */
322
+ export function previewSketchStyle(style: SketchStyle | undefined): void {
323
+ paintPreviewRoots(style);
324
+ }
325
+
326
+ /** Undo a sketchstyle preview by repainting the live buffer, which the
327
+ preview never touched. Re-deriving from the current stores rather than a
328
+ scraped snapshot means it can't drift from what they actually hold by the
329
+ time the picker closes. */
330
+ export function revertSketchStylePreview(): void {
331
+ if (get(sketchEnabled)) {
332
+ paintPreviewRoots(get(sketchSettings));
333
+ return;
334
+ }
335
+ // The one case with nodes to take down: a sketched preview over a crisp live
336
+ // state injected the sheet. `render`'s sweep is safe here because every
337
+ // component scope answers to `sketchEnabled`, which is false.
338
+ render(false, get(sketchSettings));
339
+ }
340
+
188
341
  export function setSketchPageRoot(el: HTMLElement | null): void {
189
342
  if (el === pageRoot) return;
190
343
  setSketchScope(pageRoot, null);
@@ -226,13 +379,14 @@ if (typeof document !== 'undefined') {
226
379
  render(get(sketchEnabled), settings);
227
380
  });
228
381
 
229
- sketchPreset.subscribe((name) => share(PRESET_KEY, name));
382
+ sketchStyleName.subscribe((name) => share(STYLE_NAME_KEY, name));
230
383
  sketchBaseline.subscribe((b) => share(BASELINE_KEY, b ? JSON.stringify(b) : ''));
231
384
 
232
- /* The overlay editor runs in an iframe, so the Sketch tab and a control on
233
- the page are separate instances of this module. localStorage is the ground
234
- they share: each adopts a value only when it differs from what it holds,
235
- and records what it adopted so it never sends that value back. */
385
+ /* The overlay editor runs in an iframe, so the Sketchstyle view and a
386
+ control on the page are separate instances of this module. localStorage
387
+ is the ground they share: each adopts a value only when it differs from
388
+ what it holds, and records what it adopted so it never sends that value
389
+ back. */
236
390
  window.addEventListener('storage', (event) => {
237
391
  if (event.storageArea !== localStorage) return;
238
392
  if (event.key === ENABLED_KEY) {
@@ -245,10 +399,10 @@ if (typeof document !== 'undefined') {
245
399
  adopt(SETTINGS_KEY, JSON.stringify(next));
246
400
  sketchSettings.set(next);
247
401
  }
248
- } else if (event.key === PRESET_KEY) {
249
- const next = readPresetName();
250
- adopt(PRESET_KEY, next);
251
- if (next !== get(sketchPreset)) sketchPreset.set(next);
402
+ } else if (event.key === STYLE_NAME_KEY) {
403
+ const next = readStyleName();
404
+ adopt(STYLE_NAME_KEY, next);
405
+ if (next !== get(sketchStyleName)) sketchStyleName.set(next);
252
406
  } else if (event.key === BASELINE_KEY) {
253
407
  const current = get(sketchBaseline);
254
408
  if ((event.newValue || '') !== (current ? JSON.stringify(current) : '')) {
@@ -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
@@ -119,7 +119,7 @@ export interface SketchSettings {
119
119
  iconMaskScale: number;
120
120
  }
121
121
 
122
- const base: SketchSettings = {
122
+ const base: SketchStyle = {
123
123
  label: '', blurb: '',
124
124
  fillTravel: 1.5, strokeTravel: 1.5, wobble: 45, roughness: 3,
125
125
  borderWavelength: 1, waveform: 1,
@@ -133,7 +133,7 @@ const base: SketchSettings = {
133
133
  iconTravel: 1.25, iconWavelength: 0.625, iconMaskOn: true, iconMaskScale: 1,
134
134
  };
135
135
 
136
- export const SKETCH_PRESETS: Record<string, SketchSettings> = {
136
+ export const SKETCH_STYLES: Record<string, SketchStyle> = {
137
137
  pencil: {
138
138
  ...base, label: 'Pencil',
139
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.',
@@ -214,17 +214,17 @@ export const SKETCH_PRESETS: Record<string, SketchSettings> = {
214
214
  },
215
215
  };
216
216
 
217
- export const DEFAULT_SKETCH_PRESET = 'marker';
217
+ export const DEFAULT_SKETCH_STYLE = 'marker';
218
218
 
219
- /** 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
220
220
  control existed picks up the default, and a value stored for a control since
221
221
  retired is dropped. Without the drop, a stale key survives every spread and
222
222
  makes the settings compare unequal to any baseline forever. */
223
- export function hydrateSketchSettings(raw: unknown): SketchSettings {
224
- const base = SKETCH_PRESETS[DEFAULT_SKETCH_PRESET];
225
- 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>;
226
226
  const out = { ...base };
227
- for (const key of Object.keys(base) as (keyof SketchSettings)[]) {
227
+ for (const key of Object.keys(base) as (keyof SketchStyle)[]) {
228
228
  if (stored[key] !== undefined) (out[key] as unknown) = stored[key];
229
229
  }
230
230
  // A retired option: the fill's presence belongs to the theme, not the effect.
@@ -241,8 +241,8 @@ export function hydrateSketchSettings(raw: unknown): SketchSettings {
241
241
 
242
242
  /** The second pass used to sit at a distance derived from the stroke width,
243
243
  with no dial of its own. A look stored before the dial comes back at that
244
- distance rather than at whatever the fallback preset happens to carry. */
245
- 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 {
246
246
  if (stored.retraceOffset === undefined) {
247
247
  out.retraceOffset = Number(Math.max(1.2, out.strokeWidth * 0.55).toFixed(2));
248
248
  }
@@ -262,7 +262,7 @@ const SWING_DIALS = {
262
262
  /** The pen wobble used to be stated as `frequency`, in cycles per px, which is
263
263
  the number the filter wants and not one anybody can picture. It is a
264
264
  wavelength in px now, the way the mask states its blobs. */
265
- function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchSettings): void {
265
+ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchStyle): void {
266
266
  const cycles = stored.frequency;
267
267
  if (typeof cycles === 'number' && cycles > 0) out.wobble = Math.round(1 / cycles);
268
268
  // The layer count was `octaves`, and its dial ran to 5. The top two moved
@@ -285,13 +285,13 @@ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchS
285
285
  of the field, so it came out either untouched or gone. It is a share of the
286
286
  glyph now, and the old default reads as one period across the glyph, which
287
287
  is what that size was aiming at. */
288
- function convertIconTileToScale(stored: Record<string, unknown>, out: SketchSettings): void {
288
+ function convertIconTileToScale(stored: Record<string, unknown>, out: SketchStyle): void {
289
289
  const tile = stored.iconMaskTile;
290
290
  if (typeof tile !== 'number') return;
291
291
  out.iconMaskScale = Math.min(5, Math.max(0.5, Number((tile / 90).toFixed(2))));
292
292
  }
293
293
 
294
- function halveSwingDials(stored: Record<string, unknown>, out: SketchSettings): void {
294
+ function halveSwingDials(stored: Record<string, unknown>, out: SketchStyle): void {
295
295
  for (const [legacy, key] of Object.entries(SWING_DIALS)) {
296
296
  const value = stored[legacy];
297
297
  if (typeof value === 'number') out[key as 'fillTravel'] = value / 2;
@@ -301,7 +301,7 @@ function halveSwingDials(stored: Record<string, unknown>, out: SketchSettings):
301
301
  /** The mask used to be a 600-unit tile painted at `maskScale` px, with the
302
302
  coverage point buried in the hardness slope. Recover page-px blobs and
303
303
  softness, and the levels the old slope and floor put the edge at. */
304
- function convertTiledMask(stored: Record<string, unknown>, out: SketchSettings): void {
304
+ function convertTiledMask(stored: Record<string, unknown>, out: SketchStyle): void {
305
305
  const scale = stored.maskScale;
306
306
  if (typeof scale !== 'number') return;
307
307
  const freq = stored.maskFrequency;
@@ -316,7 +316,7 @@ function convertTiledMask(stored: Record<string, unknown>, out: SketchSettings):
316
316
  * levels the pair put the edge between, and rescale them onto the field as it
317
317
  * is now: stretched onto its full range before the levels see it.
318
318
  */
319
- function convertCutToLevels(stored: Record<string, unknown>, out: SketchSettings): void {
319
+ function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyle): void {
320
320
  const contrast = stored.maskContrast;
321
321
  const coverage = stored.maskCoverage;
322
322
  if (typeof contrast !== 'number' || typeof coverage !== 'number') return;
@@ -332,7 +332,7 @@ function convertCutToLevels(stored: Record<string, unknown>, out: SketchSettings
332
332
  now, the palest and densest the fill gets. The same two numbers carry over:
333
333
  a look cut between 20% and 50% comes back as a wash between 20% and 50% ink,
334
334
  which keeps its spread and loses only the hole. */
335
- function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchSettings): void {
335
+ function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchStyle): void {
336
336
  if (typeof stored.maskLevelMin === 'number') out.maskOutputMin = stored.maskLevelMin;
337
337
  if (typeof stored.maskLevelMax === 'number') out.maskOutputMax = stored.maskLevelMax;
338
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
  }