@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.
- package/.claude/skills/live-tokens-adjust-geometry/SKILL.md +4 -4
- package/.claude/skills/live-tokens-build-page/SKILL.md +21 -3
- package/.claude/skills/live-tokens-create-component/SKILL.md +16 -48
- package/.claude/skills/live-tokens-create-component/references/fixed-overlays.md +10 -1
- package/.claude/skills/live-tokens-create-component/references/intrinsics.md +7 -5
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +1 -1
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +50 -0
- package/.claude/skills/live-tokens-generate-theme/SKILL.md +16 -9
- package/.claude/skills/live-tokens-pair-fonts/SKILL.md +8 -6
- package/.claude/skills/live-tokens-pick-component/SKILL.md +19 -5
- package/CHANGELOG.md +41 -0
- package/bin/migrate.mjs +6 -2
- package/dist-plugin/adjust/index.cjs +1 -1
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/adjust/index.js +1 -1
- package/dist-plugin/{chunk-232GZGQU.js → chunk-NDJJORKJ.js} +342 -5
- package/dist-plugin/{chunk-Y5CNFSSV.js → chunk-RVE3MNKM.js} +1 -1
- package/dist-plugin/{chunk-OPYOK2CA.js → chunk-ZHPX7ZYQ.js} +39 -1
- package/dist-plugin/{dataPaths-CRfD1LdA.d.ts → dataPaths-DZUzVv8H.d.cts} +3 -3
- package/dist-plugin/{dataPaths-CRfD1LdA.d.cts → dataPaths-DZUzVv8H.d.ts} +3 -3
- package/dist-plugin/fontPairing/index.cjs +1 -1
- package/dist-plugin/fontPairing/index.d.cts +1 -1
- package/dist-plugin/fontPairing/index.d.ts +1 -1
- package/dist-plugin/fontPairing/index.js +1 -1
- package/dist-plugin/generateColorsAndType/index.cjs +1 -1
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.js +1 -1
- package/dist-plugin/index.cjs +417 -34
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +31 -23
- package/dist-plugin/migrateData/index.cjs +350 -9
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +9 -5
- package/dist-plugin/tokensCssMigrations/index.cjs +39 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/dist-plugin/tokensCssMigrations/index.js +2 -2
- package/package.json +4 -2
- package/src/app/site.css +16 -0
- package/src/editor/core/preview/lookPreview.ts +46 -5
- package/src/editor/core/productionPulse.ts +6 -2
- package/src/editor/core/sketch/maskField.ts +5 -5
- package/src/editor/core/sketch/sketchLayer.ts +8 -8
- package/src/editor/core/sketch/sketchStore.ts +233 -79
- package/src/editor/core/sketch/{sketchPresetService.ts → sketchStyleService.ts} +14 -14
- package/src/editor/core/sketch/{sketchPresets.ts → sketchStyles.ts} +17 -17
- package/src/editor/core/themes/themeDocumentSync.ts +2 -0
- package/src/editor/core/themes/themeInit.ts +19 -1
- package/src/editor/core/themes/themeService.ts +7 -2
- package/src/editor/core/themes/themeTypes.ts +5 -0
- package/src/editor/docs/content/editing-tokens.md +1 -1
- package/src/editor/docs/content/sketch-mode.md +27 -16
- package/src/editor/docs/content/themes-workflow.md +30 -21
- package/src/editor/docs/content/where-themes-live.md +9 -5
- package/src/editor/docs/content.generated.ts +4 -4
- package/src/editor/overlay/LiveTokensRouter.svelte +1 -1
- package/src/editor/ui/EditorViewSwitcher.svelte +3 -3
- package/src/editor/ui/ThemePanel.svelte +47 -1
- package/src/editor/ui/sections/textStyles.ts +15 -1
- package/src/editor/ui/sketch/SketchPreview.svelte +3 -3
- package/src/editor/ui/sketch/SketchTab.svelte +87 -53
- package/src/system/components/FloatingTokenTags.css +10 -8
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
} from './
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type
|
|
16
|
-
} from './
|
|
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
|
|
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():
|
|
78
|
+
function readSettings(): SketchStyle {
|
|
32
79
|
try {
|
|
33
80
|
const raw = localStorage.getItem(SETTINGS_KEY);
|
|
34
|
-
if (raw) return
|
|
81
|
+
if (raw) return hydrateSketchStyle(JSON.parse(raw));
|
|
35
82
|
} catch {
|
|
36
|
-
// fall through to the default
|
|
83
|
+
// fall through to the default sketchstyle
|
|
37
84
|
}
|
|
38
|
-
return { ...
|
|
85
|
+
return { ...SKETCH_STYLES[DEFAULT_SKETCH_STYLE] };
|
|
39
86
|
}
|
|
40
87
|
|
|
41
|
-
function readBaseline():
|
|
88
|
+
function readBaseline(): SketchStyle | null {
|
|
42
89
|
try {
|
|
43
90
|
const raw = localStorage.getItem(BASELINE_KEY);
|
|
44
|
-
if (raw) return
|
|
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
|
|
52
|
-
the shipped `pencil` stay distinguishable in one string. */
|
|
53
|
-
export const
|
|
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
|
|
102
|
+
function readStyleName(): string {
|
|
56
103
|
try {
|
|
57
|
-
const name = localStorage.getItem(
|
|
58
|
-
if (name === '' || (name && (name in
|
|
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
|
|
111
|
+
return DEFAULT_SKETCH_STYLE;
|
|
65
112
|
}
|
|
66
113
|
|
|
67
|
-
/** Off by default: the effect
|
|
68
|
-
|
|
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<
|
|
71
|
-
/** The
|
|
72
|
-
showing what the current look is closest to; empty only when nothing
|
|
73
|
-
picked, or the picked file was deleted. */
|
|
74
|
-
export const
|
|
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
|
|
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<
|
|
127
|
+
export const sketchBaseline = writable<SketchStyle | null>(readBaseline());
|
|
81
128
|
|
|
82
|
-
/** Dial-set fields only. `label` and `blurb` name the
|
|
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:
|
|
85
|
-
return (Object.keys(a) as (keyof
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
123
|
-
export async function
|
|
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
|
|
126
|
-
const fileName =
|
|
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
|
|
130
|
-
await
|
|
236
|
+
await saveSketchStyle(fileName, trimmed, settings);
|
|
237
|
+
await refreshSavedSketchStyles();
|
|
131
238
|
sketchSettings.set(settings);
|
|
132
239
|
sketchBaseline.set({ ...settings });
|
|
133
|
-
|
|
240
|
+
sketchStyleName.set(USER_STYLE_PREFIX + fileName);
|
|
134
241
|
return fileName;
|
|
135
242
|
}
|
|
136
243
|
|
|
137
|
-
export async function
|
|
138
|
-
await
|
|
139
|
-
await
|
|
140
|
-
// The dials keep their values; only the
|
|
141
|
-
if (get(
|
|
142
|
-
|
|
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
|
|
149
|
-
drift. Save writes a new
|
|
150
|
-
export function updateSketchSettings(patch: Partial<
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
233
|
-
the page are separate instances of this module. localStorage
|
|
234
|
-
they share: each adopts a value only when it differs from
|
|
235
|
-
and records what it adopted so it never sends that value
|
|
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 ===
|
|
249
|
-
const next =
|
|
250
|
-
adopt(
|
|
251
|
-
if (next !== get(
|
|
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 {
|
|
2
|
+
import { hydrateSketchStyle, type SketchStyle } from './sketchStyles';
|
|
3
3
|
|
|
4
|
-
export interface
|
|
4
|
+
export interface SketchStyleFile {
|
|
5
5
|
name: string;
|
|
6
6
|
createdAt?: string;
|
|
7
7
|
updatedAt?: string;
|
|
8
|
-
settings:
|
|
8
|
+
settings: SketchStyle;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface
|
|
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-
|
|
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
|
|
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
|
|
29
|
+
export async function listSketchStyles(): Promise<SketchStyleMeta[]> {
|
|
30
30
|
const res = await fetch(BASE);
|
|
31
|
-
if (!res.ok) throw new Error('Failed to list
|
|
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
|
|
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
|
|
38
|
+
if (!res.ok) throw new Error(`Failed to load sketchstyle: ${fileName}`);
|
|
39
39
|
const body = await res.json();
|
|
40
|
-
return { ...body, settings:
|
|
40
|
+
return { ...body, settings: hydrateSketchStyle(body.settings) };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export async function
|
|
43
|
+
export async function saveSketchStyle(
|
|
44
44
|
fileName: string,
|
|
45
45
|
name: string,
|
|
46
|
-
settings:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
217
|
+
export const DEFAULT_SKETCH_STYLE = 'marker';
|
|
218
218
|
|
|
219
|
-
/** Reconciled against a full
|
|
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
|
|
224
|
-
const base =
|
|
225
|
-
const stored = (raw ?? {}) as Partial<
|
|
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
|
|
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
|
|
245
|
-
function restoreDerivedRetrace(stored: Record<string, unknown>, out:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
}
|