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