@motion-proto/live-tokens 0.67.0 → 0.68.1
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/CHANGELOG.md +54 -0
- package/README.md +67 -9
- package/bin/generate-theme.mjs +5 -4
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/{chunk-T4PMCFJN.js → chunk-2UX6EVVA.js} +30 -30
- package/dist-plugin/{dataPaths-DZUzVv8H.d.cts → dataPaths-bJTCEO4H.d.cts} +1 -1
- package/dist-plugin/{dataPaths-DZUzVv8H.d.ts → dataPaths-bJTCEO4H.d.ts} +1 -1
- package/dist-plugin/fontPairing/index.d.cts +1 -1
- package/dist-plugin/fontPairing/index.d.ts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
- package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
- package/dist-plugin/index.cjs +32 -32
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +5 -5
- package/dist-plugin/migrateData/index.cjs +30 -30
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
- package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/editor/bootstrap.ts +6 -6
- package/src/editor/component-editor/ImageLightboxEditor.svelte +1 -1
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +4 -4
- package/src/editor/core/fonts/fontLoader.ts +2 -2
- package/src/editor/core/preview/{lookPreview.ts → themePreview.ts} +27 -27
- package/src/editor/core/productionPulse.ts +3 -3
- package/src/editor/core/sketch/index.ts +52 -44
- package/src/editor/core/sketch/maskField.ts +9 -9
- package/src/editor/core/sketch/sketchLayer.ts +9 -9
- package/src/editor/core/sketch/sketchRegistry.ts +34 -34
- package/src/editor/core/sketch/sketchStore.ts +98 -83
- package/src/editor/core/sketch/sketchStyleService.ts +4 -4
- package/src/editor/core/sketch/sketchStyles.ts +28 -28
- package/src/editor/core/themes/colorsAndTypeService.ts +1 -1
- package/src/editor/core/themes/loadRows.ts +8 -8
- package/src/editor/core/themes/themeDocumentSync.ts +2 -2
- package/src/editor/core/themes/themeInit.ts +2 -2
- package/src/editor/core/themes/themeService.ts +17 -17
- package/src/editor/core/themes/{lookSummary.ts → themeSummary.ts} +9 -9
- package/src/editor/core/themes/themeTypes.ts +10 -6
- package/src/editor/docs/content/getting-started.md +1 -1
- package/src/editor/docs/content/sketch-mode.md +22 -17
- package/src/editor/docs/content/themes-workflow.md +11 -11
- package/src/editor/docs/content/where-themes-live.md +6 -6
- package/src/editor/docs/content.generated.ts +4 -4
- package/src/editor/index.ts +2 -2
- package/src/editor/ui/ThemePanel.svelte +52 -52
- package/src/editor/ui/sketch/SketchPreview.svelte +2 -2
- package/src/editor/ui/sketch/SketchTab.svelte +64 -38
- package/src/live-tokens/data/sketch-styles/dry.json +5 -5
- package/src/live-tokens/data/sketch-styles/hatched.json +9 -9
- package/src/live-tokens/data/sketch-styles/napkin.json +4 -4
- package/src/live-tokens/data/sketch-styles/pencil.json +2 -2
- package/src/live-tokens/data/themes/autumn.json +1 -1
- package/src/live-tokens/data/themes/halloween.json +1 -1
- package/src/live-tokens/data/themes/midnight-study.json +1 -1
- package/src/live-tokens/data/themes/ocean.json +1 -1
- package/src/live-tokens/data/themes/royal-velvet.json +1 -1
- package/src/live-tokens/data/themes/sketchy.json +1 -1
- package/src/live-tokens/data/themes/spring-meadow.json +1 -1
- package/src/live-tokens/data/themes/sunset.json +1 -1
- package/src/system/backdrop/backdrop.ts +1 -1
- package/src/system/components/SectionDivider.svelte +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { derived, get, writable } from 'svelte/store';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
SHIPPED_SKETCH_SETTINGS,
|
|
4
4
|
DEFAULT_SKETCH_STYLE,
|
|
5
5
|
THEME_SKETCH_ID,
|
|
6
|
-
|
|
7
|
-
type
|
|
6
|
+
hydrateSketchSettings,
|
|
7
|
+
type SketchStyleSettings,
|
|
8
8
|
} from './sketchStyles';
|
|
9
|
-
import {
|
|
9
|
+
import { sketchStyleById, replaceRegisteredSketchStyles, sketchStyles } from './sketchRegistry';
|
|
10
10
|
import {
|
|
11
11
|
applySketchLayer,
|
|
12
12
|
hostRoot,
|
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
|
|
27
27
|
const ENABLED_KEY = 'lt.sketchEnabled';
|
|
28
28
|
const SETTINGS_KEY = 'lt.sketchSettings';
|
|
29
|
-
|
|
29
|
+
/** The stored key keeps its old spelling. It is a browser's saved selection,
|
|
30
|
+
so renaming the string would drop every visitor's pick on upgrade. */
|
|
31
|
+
const SELECTED_ID_KEY = 'lt.sketchStyleName';
|
|
30
32
|
const BASELINE_KEY = 'lt.sketchBaseline';
|
|
31
33
|
/** Set only by a genuine user decision (`markSketchTouched`), never by the
|
|
32
34
|
`subscribe` write-back below. That write-back plants the four keys above
|
|
@@ -66,7 +68,7 @@ export function hasPersistedSketchState(): boolean {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** Called from every control a user can act on: a dial, a sketchstyle pick, the
|
|
69
|
-
on/off switch. Never from `
|
|
71
|
+
on/off switch. Never from `openThemeSketchSettings` or the storage-sync `adopt`
|
|
70
72
|
path, both of which write the same four keys as a side effect of state
|
|
71
73
|
this browser did not decide on its own. */
|
|
72
74
|
function markSketchTouched(): void {
|
|
@@ -83,20 +85,20 @@ function readEnabled(): boolean {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
function readSettings():
|
|
88
|
+
function readSettings(): SketchStyleSettings {
|
|
87
89
|
try {
|
|
88
90
|
const raw = localStorage.getItem(SETTINGS_KEY);
|
|
89
|
-
if (raw) return
|
|
91
|
+
if (raw) return hydrateSketchSettings(JSON.parse(raw));
|
|
90
92
|
} catch {
|
|
91
93
|
// fall through to the default sketchstyle
|
|
92
94
|
}
|
|
93
|
-
return { ...
|
|
95
|
+
return { ...SHIPPED_SKETCH_SETTINGS[DEFAULT_SKETCH_STYLE] };
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function readBaseline():
|
|
98
|
+
function readBaseline(): SketchStyleSettings | null {
|
|
97
99
|
try {
|
|
98
100
|
const raw = localStorage.getItem(BASELINE_KEY);
|
|
99
|
-
if (raw) return
|
|
101
|
+
if (raw) return hydrateSketchSettings(JSON.parse(raw));
|
|
100
102
|
} catch {
|
|
101
103
|
// fall through
|
|
102
104
|
}
|
|
@@ -108,14 +110,14 @@ function readBaseline(): SketchStyle | null {
|
|
|
108
110
|
it. Stripped on read below; delete a release after that ships. */
|
|
109
111
|
const RETIRED_USER_PREFIX = 'user:';
|
|
110
112
|
|
|
111
|
-
/** Deliberately unvalidated.
|
|
113
|
+
/** Deliberately unvalidated. Sketchstyles are registered after this module is
|
|
112
114
|
imported, so an id it has never heard of is the normal case rather than a
|
|
113
115
|
fault: `selectSketchStyle` no-ops on one, and `sketchPick` already reports
|
|
114
|
-
a
|
|
116
|
+
a style nothing names as `adjusted`. Only a browser that has stored nothing
|
|
115
117
|
falls back. */
|
|
116
|
-
function
|
|
118
|
+
function readSelectedId(): string {
|
|
117
119
|
try {
|
|
118
|
-
const name = localStorage.getItem(
|
|
120
|
+
const name = localStorage.getItem(SELECTED_ID_KEY);
|
|
119
121
|
if (name !== null) {
|
|
120
122
|
return name.startsWith(RETIRED_USER_PREFIX) ? name.slice(RETIRED_USER_PREFIX.length) : name;
|
|
121
123
|
}
|
|
@@ -128,39 +130,39 @@ function readStyleName(): string {
|
|
|
128
130
|
/** Off by default: the effect paints nothing until the open theme's own
|
|
129
131
|
sketchstyle, or a decision this browser already recorded, says otherwise. */
|
|
130
132
|
export const sketchEnabled = writable<boolean>(readEnabled());
|
|
131
|
-
export const sketchSettings = writable<
|
|
132
|
-
/**
|
|
133
|
-
keeps showing what the current
|
|
134
|
-
`THEME_SKETCH_ID` for
|
|
135
|
-
nothing was picked, or the picked file was deleted. */
|
|
136
|
-
export const
|
|
133
|
+
export const sketchSettings = writable<SketchStyleSettings>(readSettings());
|
|
134
|
+
/** Id of the sketchstyle the dials started from. It survives dial moves, so the
|
|
135
|
+
grid keeps showing what the current settings are closest to. Any id in the
|
|
136
|
+
pool, or `THEME_SKETCH_ID` for settings the open theme carries that no file
|
|
137
|
+
holds; empty only when nothing was picked, or the picked file was deleted. */
|
|
138
|
+
export const selectedSketchStyleId = writable<string>(readSelectedId());
|
|
137
139
|
|
|
138
140
|
/** The settings as the selected sketchstyle defined them. Kept beside the live
|
|
139
141
|
settings so "modified" is derived by comparison rather than tracked as a
|
|
140
142
|
flag: dial a value back to where it started and the tab stops claiming a
|
|
141
143
|
change, with no bookkeeping to get wrong. */
|
|
142
|
-
export const sketchBaseline = writable<
|
|
144
|
+
export const sketchBaseline = writable<SketchStyleSettings | null>(readBaseline());
|
|
143
145
|
|
|
144
146
|
/** Dial-set fields only. `label` and `blurb` name the sketchstyle rather than
|
|
145
|
-
describe the
|
|
146
|
-
export function
|
|
147
|
-
return (Object.keys(a) as (keyof
|
|
147
|
+
describe the style, and no dial writes them. */
|
|
148
|
+
export function sameSketchStyle(a: SketchStyleSettings, b: SketchStyleSettings): boolean {
|
|
149
|
+
return (Object.keys(a) as (keyof SketchStyleSettings)[])
|
|
148
150
|
.filter((k) => k !== 'label' && k !== 'blurb')
|
|
149
151
|
.every((k) => a[k] === b[k]);
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
export const sketchDirty = derived(
|
|
153
155
|
[sketchSettings, sketchBaseline],
|
|
154
|
-
([settings, baseline]) => baseline !== null && !
|
|
156
|
+
([settings, baseline]) => baseline !== null && !sameSketchStyle(settings, baseline),
|
|
155
157
|
);
|
|
156
158
|
|
|
157
159
|
export const sketchBlurb = derived(sketchSettings, (s) => s.blurb);
|
|
158
160
|
|
|
159
|
-
/** The live
|
|
161
|
+
/** The live style as a theme would carry it: the dials when the effect is
|
|
160
162
|
on, nothing when it is off (RJC 1). A copy, not the store's own object,
|
|
161
163
|
so a caller holding onto the result can never observe a later dial move
|
|
162
164
|
through it. */
|
|
163
|
-
export function
|
|
165
|
+
export function liveSketchSettings(): SketchStyleSettings | undefined {
|
|
164
166
|
return get(sketchEnabled) ? { ...get(sketchSettings) } : undefined;
|
|
165
167
|
}
|
|
166
168
|
|
|
@@ -175,25 +177,25 @@ export function setSketchEnabled(enabled: boolean): void {
|
|
|
175
177
|
|
|
176
178
|
/** What the open theme holds, so "unsaved" is a comparison rather than a
|
|
177
179
|
flag (RJC 5). Set by every path that opens or saves a theme. */
|
|
178
|
-
export const
|
|
180
|
+
export const themeSketchSettings = writable<SketchStyleSettings | undefined>(undefined);
|
|
179
181
|
|
|
180
182
|
/** Open a theme's sketchstyle: the dials, the on/off state, and the name
|
|
181
183
|
recovered by comparison (RJC 3). Overwrites the live buffer, which
|
|
182
184
|
is what opening a theme means everywhere else (RJC 6). The name is recovered
|
|
183
|
-
over the whole pool, so a theme carrying a
|
|
185
|
+
over the whole pool, so a theme carrying a style a saved file also holds is
|
|
184
186
|
named by that file rather than falling back to `THEME_SKETCH_ID`. */
|
|
185
|
-
export function
|
|
186
|
-
|
|
187
|
-
if (!
|
|
187
|
+
export function openThemeSketchSettings(settings: SketchStyleSettings | undefined): void {
|
|
188
|
+
themeSketchSettings.set(settings);
|
|
189
|
+
if (!settings) {
|
|
188
190
|
sketchEnabled.set(false);
|
|
189
191
|
sketchBaseline.set(null);
|
|
190
|
-
|
|
192
|
+
selectedSketchStyleId.set('');
|
|
191
193
|
return;
|
|
192
194
|
}
|
|
193
|
-
const matched = get(
|
|
194
|
-
sketchSettings.set({ ...
|
|
195
|
-
sketchBaseline.set({ ...
|
|
196
|
-
|
|
195
|
+
const matched = get(sketchStyles).find((style) => sameSketchStyle(style.settings, settings))?.id;
|
|
196
|
+
sketchSettings.set({ ...settings });
|
|
197
|
+
sketchBaseline.set({ ...settings });
|
|
198
|
+
selectedSketchStyleId.set(matched ?? THEME_SKETCH_ID);
|
|
197
199
|
sketchEnabled.set(true);
|
|
198
200
|
}
|
|
199
201
|
|
|
@@ -202,40 +204,40 @@ export function openThemeSketchStyle(sketchStyle: SketchStyle | undefined): void
|
|
|
202
204
|
* browser has already decided for itself.
|
|
203
205
|
*
|
|
204
206
|
* The rule boot has always followed in dev, and the only one a built site has:
|
|
205
|
-
* a visitor who picked a
|
|
207
|
+
* a visitor who picked a style, or picked None, keeps it, and `themeSketchSettings`
|
|
206
208
|
* still learns what the theme holds so the panel can call the difference
|
|
207
|
-
* unsaved. Both branches set it, so a picker can offer the theme's
|
|
209
|
+
* unsaved. Both branches set it, so a picker can offer the theme's style as a
|
|
208
210
|
* row either way.
|
|
209
211
|
*
|
|
210
|
-
* Takes the raw field rather than a `
|
|
212
|
+
* Takes the raw field rather than a `SketchStyleSettings`, and hydrates it here: a
|
|
211
213
|
* built site reads its theme JSON straight off disk with no dev server to run
|
|
212
|
-
* `normalizeTheme` over it first, so this is the only place a
|
|
214
|
+
* `normalizeTheme` over it first, so this is the only place a style stored under
|
|
213
215
|
* a retired dial name gets carried forward. Anything that is not an object is
|
|
214
216
|
* the absent case, which is off (invariant 3).
|
|
215
217
|
*/
|
|
216
|
-
export function seedSketchFromTheme(
|
|
218
|
+
export function seedSketchFromTheme(raw: unknown): void {
|
|
217
219
|
const style =
|
|
218
|
-
typeof
|
|
219
|
-
?
|
|
220
|
+
typeof raw === 'object' && raw !== null && !Array.isArray(raw)
|
|
221
|
+
? hydrateSketchSettings(raw)
|
|
220
222
|
: undefined;
|
|
221
223
|
if (hasPersistedSketchState()) {
|
|
222
|
-
|
|
224
|
+
themeSketchSettings.set(style);
|
|
223
225
|
return;
|
|
224
226
|
}
|
|
225
|
-
|
|
227
|
+
openThemeSketchSettings(style);
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
/** Go back to the
|
|
229
|
-
theme's is the one
|
|
230
|
+
/** Go back to the style the theme carries, after picking something else. The
|
|
231
|
+
theme's is the one style a picker can offer that this module did not ship, so
|
|
230
232
|
it needs a door of its own beside `selectSketchStyle`; `setSketch` gives the
|
|
231
233
|
two the same face. Silent when the theme carries none, the way
|
|
232
234
|
`selectSketchStyle` is for a name it does not know. */
|
|
233
|
-
export function
|
|
234
|
-
const style = get(
|
|
235
|
+
export function selectUnsavedSketchStyle(): void {
|
|
236
|
+
const style = get(themeSketchSettings);
|
|
235
237
|
if (!style) return;
|
|
236
238
|
markSketchTouched();
|
|
237
239
|
if (get(sketchEnabled)) liveMovedSinceBake.set(true);
|
|
238
|
-
|
|
240
|
+
selectedSketchStyleId.set(THEME_SKETCH_ID);
|
|
239
241
|
sketchBaseline.set({ ...style });
|
|
240
242
|
sketchSettings.set({ ...style });
|
|
241
243
|
}
|
|
@@ -243,13 +245,13 @@ export function selectThemeSketchStyle(): void {
|
|
|
243
245
|
/** The live sketch differs from what the open theme carries. Presence is
|
|
244
246
|
half the comparison: on with dials the theme does not hold, or off while
|
|
245
247
|
the theme holds a layer, are both off the theme. */
|
|
246
|
-
export const
|
|
247
|
-
[sketchEnabled, sketchSettings,
|
|
248
|
+
export const sketchOffTheme = derived(
|
|
249
|
+
[sketchEnabled, sketchSettings, themeSketchSettings],
|
|
248
250
|
([enabled, settings, saved]) => {
|
|
249
251
|
const live = enabled ? settings : undefined;
|
|
250
252
|
if (!live && !saved) return false;
|
|
251
253
|
if (!live || !saved) return true;
|
|
252
|
-
return !
|
|
254
|
+
return !sameSketchStyle(live, saved);
|
|
253
255
|
},
|
|
254
256
|
);
|
|
255
257
|
|
|
@@ -257,13 +259,13 @@ export const sketchOffLook = derived(
|
|
|
257
259
|
Silent for an id nothing knows, the way it has always been for an unknown
|
|
258
260
|
shipped name. */
|
|
259
261
|
export function selectSketchStyle(id: string): void {
|
|
260
|
-
const
|
|
261
|
-
if (!
|
|
262
|
+
const style = sketchStyleById(id);
|
|
263
|
+
if (!style) return;
|
|
262
264
|
markSketchTouched();
|
|
263
265
|
if (get(sketchEnabled)) liveMovedSinceBake.set(true);
|
|
264
|
-
|
|
265
|
-
sketchBaseline.set({ ...
|
|
266
|
-
sketchSettings.set({ ...
|
|
266
|
+
selectedSketchStyleId.set(id);
|
|
267
|
+
sketchBaseline.set({ ...style.settings });
|
|
268
|
+
sketchSettings.set({ ...style.settings });
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
/** Saved sketchstyles, listed from the data tree. Empty until
|
|
@@ -279,7 +281,7 @@ export async function refreshSavedSketchStyles(): Promise<void> {
|
|
|
279
281
|
const files = await listSketchStyles();
|
|
280
282
|
const loaded = await Promise.all(files.map((f) => loadSketchStyle(f.fileName)));
|
|
281
283
|
savedSketchStyles.set(files);
|
|
282
|
-
|
|
284
|
+
replaceRegisteredSketchStyles(
|
|
283
285
|
files.map((file, i) => ({
|
|
284
286
|
id: file.fileName,
|
|
285
287
|
label: file.name || file.fileName,
|
|
@@ -303,23 +305,36 @@ export async function saveCurrentSketchStyle(name: string): Promise<string> {
|
|
|
303
305
|
await refreshSavedSketchStyles();
|
|
304
306
|
sketchSettings.set(settings);
|
|
305
307
|
sketchBaseline.set({ ...settings });
|
|
306
|
-
|
|
308
|
+
selectedSketchStyleId.set(fileName);
|
|
307
309
|
return fileName;
|
|
308
310
|
}
|
|
309
311
|
|
|
310
|
-
/** Overwrite the selected
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
/** Overwrite the selected sketchstyle, so a style can be put down and picked up
|
|
313
|
+
again across sessions.
|
|
314
|
+
|
|
315
|
+
The file name comes from the selection rather than from re-slugifying the
|
|
316
|
+
label, because the two can disagree: `dry` is labelled "Dry marker", which
|
|
317
|
+
re-slugified writes `dry-marker.json` and leaves the selected file untouched.
|
|
318
|
+
The label comes from the style for the same reason, so the name the grid shows
|
|
319
|
+
survives.
|
|
320
|
+
|
|
321
|
+
A shipped style saves the same way. Its file lives in the package, so the
|
|
322
|
+
write lands in this project under the same id and shadows it, which is how a
|
|
323
|
+
project comes to own its Pencil; deleting that file uncovers the packaged one
|
|
324
|
+
again. A registered style owns no file at all, so it has none to write over.
|
|
315
325
|
|
|
316
326
|
No `markSketchTouched`: the button only lights once a dial has moved, and
|
|
317
327
|
every dial goes through `updateSketchSettings`, which marks it. */
|
|
318
328
|
export async function saveSelectedSketchStyle(): Promise<void> {
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
329
|
+
const style = sketchStyleById(get(selectedSketchStyleId));
|
|
330
|
+
if (!style) throw new Error('No sketchstyle is selected');
|
|
331
|
+
if (style.source === 'registered') {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`${style.label} was registered in code rather than saved as a file. Use Save As to keep these dials.`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
321
336
|
const settings = { ...get(sketchSettings) };
|
|
322
|
-
await saveSketchStyle(
|
|
337
|
+
await saveSketchStyle(style.id, style.label, settings);
|
|
323
338
|
await refreshSavedSketchStyles();
|
|
324
339
|
// Re-baselining is what disables the button again and returns the readout
|
|
325
340
|
// from "Modified from X" to the saved blurb.
|
|
@@ -330,16 +345,16 @@ export async function deleteSavedSketchStyle(fileName: string): Promise<void> {
|
|
|
330
345
|
await deleteSketchStyle(fileName);
|
|
331
346
|
await refreshSavedSketchStyles();
|
|
332
347
|
// The dials keep their values; only the name stops naming a file that exists.
|
|
333
|
-
if (get(
|
|
334
|
-
|
|
348
|
+
if (get(selectedSketchStyleId) === fileName) {
|
|
349
|
+
selectedSketchStyleId.set('');
|
|
335
350
|
sketchBaseline.set(null);
|
|
336
351
|
}
|
|
337
352
|
}
|
|
338
353
|
|
|
339
354
|
/** Every dial goes through here. The selection deliberately survives: the grid
|
|
340
|
-
keeps naming the sketchstyle this
|
|
355
|
+
keeps naming the sketchstyle this style came from, and `sketchDirty` reports
|
|
341
356
|
the drift. Save writes a new sketchstyle rather than overwriting the base. */
|
|
342
|
-
export function updateSketchSettings(patch: Partial<
|
|
357
|
+
export function updateSketchSettings(patch: Partial<SketchStyleSettings>): void {
|
|
343
358
|
markSketchTouched();
|
|
344
359
|
if (get(sketchEnabled)) liveMovedSinceBake.set(true);
|
|
345
360
|
sketchSettings.update((s) => ({ ...s, ...patch }));
|
|
@@ -361,7 +376,7 @@ function persist(key: string, value: string): void {
|
|
|
361
376
|
itself between import and first render. */
|
|
362
377
|
let pageRoot: HTMLElement | null = null;
|
|
363
378
|
|
|
364
|
-
function render(enabled: boolean, settings:
|
|
379
|
+
function render(enabled: boolean, settings: SketchStyleSettings): void {
|
|
365
380
|
if (typeof document === 'undefined') return;
|
|
366
381
|
if (!enabled) {
|
|
367
382
|
if (sketchLayerInstalled()) removeSketchLayer();
|
|
@@ -375,14 +390,14 @@ function render(enabled: boolean, settings: SketchStyle): void {
|
|
|
375
390
|
setSketchScope(pageRoot, settings);
|
|
376
391
|
}
|
|
377
392
|
|
|
378
|
-
/** Paint one
|
|
393
|
+
/** Paint one style on the two roots `render` owns, and on nothing else.
|
|
379
394
|
|
|
380
395
|
`render(false, ...)` reaches further: it clears every `[data-sketch]` in
|
|
381
396
|
the document. The scopes it takes with it belong to `$effect`s that paint
|
|
382
397
|
from the live stores, which a preview deliberately never ticks, so those
|
|
383
398
|
never come back. Previewing a theme carrying no sketchstyle left the
|
|
384
399
|
Sketchstyle view's own stage crisp for good, Cancel included. */
|
|
385
|
-
function paintPreviewRoots(style:
|
|
400
|
+
function paintPreviewRoots(style: SketchStyleSettings | undefined): void {
|
|
386
401
|
if (typeof document === 'undefined') return;
|
|
387
402
|
if (style) applySketchLayer(style);
|
|
388
403
|
setSketchScope(hostRoot(), style ?? null);
|
|
@@ -397,7 +412,7 @@ function paintPreviewRoots(style: SketchStyle | undefined): void {
|
|
|
397
412
|
host page across the iframe boundary the same way Apply's does; only the
|
|
398
413
|
store write and the persistence are skipped. Pass `undefined` for a theme
|
|
399
414
|
that carries no sketchstyle. */
|
|
400
|
-
export function previewSketchStyle(style:
|
|
415
|
+
export function previewSketchStyle(style: SketchStyleSettings | undefined): void {
|
|
401
416
|
paintPreviewRoots(style);
|
|
402
417
|
}
|
|
403
418
|
|
|
@@ -457,7 +472,7 @@ if (typeof document !== 'undefined') {
|
|
|
457
472
|
render(get(sketchEnabled), settings);
|
|
458
473
|
});
|
|
459
474
|
|
|
460
|
-
|
|
475
|
+
selectedSketchStyleId.subscribe((name) => share(SELECTED_ID_KEY, name));
|
|
461
476
|
sketchBaseline.subscribe((b) => share(BASELINE_KEY, b ? JSON.stringify(b) : ''));
|
|
462
477
|
|
|
463
478
|
/* The overlay editor runs in an iframe, so the Sketchstyle view and a
|
|
@@ -477,10 +492,10 @@ if (typeof document !== 'undefined') {
|
|
|
477
492
|
adopt(SETTINGS_KEY, JSON.stringify(next));
|
|
478
493
|
sketchSettings.set(next);
|
|
479
494
|
}
|
|
480
|
-
} else if (event.key ===
|
|
481
|
-
const next =
|
|
482
|
-
adopt(
|
|
483
|
-
if (next !== get(
|
|
495
|
+
} else if (event.key === SELECTED_ID_KEY) {
|
|
496
|
+
const next = readSelectedId();
|
|
497
|
+
adopt(SELECTED_ID_KEY, next);
|
|
498
|
+
if (next !== get(selectedSketchStyleId)) selectedSketchStyleId.set(next);
|
|
484
499
|
} else if (event.key === BASELINE_KEY) {
|
|
485
500
|
const current = get(sketchBaseline);
|
|
486
501
|
if ((event.newValue || '') !== (current ? JSON.stringify(current) : '')) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { API_BASE } from '../storage/apiBase';
|
|
2
|
-
import {
|
|
2
|
+
import { hydrateSketchSettings, type SketchStyleSettings } from './sketchStyles';
|
|
3
3
|
|
|
4
4
|
export interface SketchStyleFile {
|
|
5
5
|
name: string;
|
|
6
6
|
createdAt?: string;
|
|
7
7
|
updatedAt?: string;
|
|
8
|
-
settings:
|
|
8
|
+
settings: SketchStyleSettings;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface SketchStyleMeta {
|
|
@@ -40,13 +40,13 @@ export async function loadSketchStyle(fileName: string): Promise<SketchStyleFile
|
|
|
40
40
|
const res = await fetch(`${BASE}/${encodeURIComponent(fileName)}`);
|
|
41
41
|
if (!res.ok) throw new Error(`Failed to load sketchstyle: ${fileName}`);
|
|
42
42
|
const body = await res.json();
|
|
43
|
-
return { ...body, settings:
|
|
43
|
+
return { ...body, settings: hydrateSketchSettings(body.settings) };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export async function saveSketchStyle(
|
|
47
47
|
fileName: string,
|
|
48
48
|
name: string,
|
|
49
|
-
settings:
|
|
49
|
+
settings: SketchStyleSettings,
|
|
50
50
|
): Promise<void> {
|
|
51
51
|
const res = await fetch(`${BASE}/${encodeURIComponent(fileName)}`, {
|
|
52
52
|
method: 'PUT',
|
|
@@ -6,7 +6,7 @@ import dashed from '../../../live-tokens/data/sketch-styles/dashed.json';
|
|
|
6
6
|
import napkin from '../../../live-tokens/data/sketch-styles/napkin.json';
|
|
7
7
|
import dry from '../../../live-tokens/data/sketch-styles/dry.json';
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface SketchStyleSettings {
|
|
10
10
|
label: string;
|
|
11
11
|
blurb: string;
|
|
12
12
|
/** How far the fill's edge travels at its furthest, in px. Every dial that
|
|
@@ -80,7 +80,7 @@ export interface SketchStyle {
|
|
|
80
80
|
/** Whether the two move together. Unlinked they part, and the field comes out
|
|
81
81
|
stretched: blobs wider than they are tall read as a wash dragged sideways,
|
|
82
82
|
the way ink pulled across a page does. Stored rather than inferred from
|
|
83
|
-
the pair matching, so a
|
|
83
|
+
the pair matching, so a sketchstyle that stretches to exactly square keeps its
|
|
84
84
|
dials apart. */
|
|
85
85
|
maskBlobLinked: boolean;
|
|
86
86
|
/** Output levels on the coverage field, 0 to 1: the palest the fill gets and
|
|
@@ -144,40 +144,40 @@ export interface SketchStyle {
|
|
|
144
144
|
/**
|
|
145
145
|
* The shipped sketchstyles, read from the files the package distributes. The
|
|
146
146
|
* files are the source: a project shadows one by saving a sketchstyle under the
|
|
147
|
-
* same id, the editor restores a shipped
|
|
147
|
+
* same id, the editor restores a shipped sketchstyle by deleting that file, and
|
|
148
148
|
* `themeFileApi` serves these as the read-only fallback behind the project's own
|
|
149
|
-
* directory. Editing a
|
|
149
|
+
* directory. Editing a sketchstyle here means editing its JSON, which is what the
|
|
150
150
|
* Sketchstyle view already writes.
|
|
151
151
|
*
|
|
152
152
|
* Each file carries every dial, so there is nothing to merge a default into.
|
|
153
153
|
* `sketchStyles.test.ts` pins that: the seven key sets have to match, and a dial
|
|
154
|
-
* added to `
|
|
154
|
+
* added to `SketchStyleSettings` has to reach all seven before the suite goes green.
|
|
155
155
|
*
|
|
156
156
|
* Order is picker order.
|
|
157
157
|
*/
|
|
158
158
|
const SHIPPED_FILES = { pencil, marker, whiteboard, hatched, dashed, napkin, dry };
|
|
159
159
|
|
|
160
|
-
export const
|
|
161
|
-
Object.entries(SHIPPED_FILES).map(([id, file]) => [id, file.settings as unknown as
|
|
160
|
+
export const SHIPPED_SKETCH_SETTINGS: Record<string, SketchStyleSettings> = Object.fromEntries(
|
|
161
|
+
Object.entries(SHIPPED_FILES).map(([id, file]) => [id, file.settings as unknown as SketchStyleSettings]),
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
export const DEFAULT_SKETCH_STYLE = 'marker';
|
|
165
165
|
|
|
166
|
-
/** The id
|
|
166
|
+
/** The id for sketch settings a theme carries, in the same id namespace as the shipped
|
|
167
167
|
sketchstyles so one picker row and one `setSketch` call cover both. Never a
|
|
168
|
-
key of `
|
|
169
|
-
own
|
|
168
|
+
key of `SHIPPED_SKETCH_SETTINGS`: a shipped style claiming it would shadow the theme's
|
|
169
|
+
own sketchstyle in every picker. `index.test.ts` pins that. */
|
|
170
170
|
export const THEME_SKETCH_ID = 'theme';
|
|
171
171
|
|
|
172
172
|
/** Reconciled against a full sketchstyle in both directions: a value stored before a
|
|
173
173
|
control existed picks up the default, and a value stored for a control since
|
|
174
174
|
retired is dropped. Without the drop, a stale key survives every spread and
|
|
175
175
|
makes the settings compare unequal to any baseline forever. */
|
|
176
|
-
export function
|
|
177
|
-
const base =
|
|
178
|
-
const stored = (raw ?? {}) as Partial<
|
|
176
|
+
export function hydrateSketchSettings(raw: unknown): SketchStyleSettings {
|
|
177
|
+
const base = SHIPPED_SKETCH_SETTINGS[DEFAULT_SKETCH_STYLE];
|
|
178
|
+
const stored = (raw ?? {}) as Partial<SketchStyleSettings>;
|
|
179
179
|
const out = { ...base };
|
|
180
|
-
for (const key of Object.keys(base) as (keyof
|
|
180
|
+
for (const key of Object.keys(base) as (keyof SketchStyleSettings)[]) {
|
|
181
181
|
if (stored[key] !== undefined) (out[key] as unknown) = stored[key];
|
|
182
182
|
}
|
|
183
183
|
// A retired option: the fill's presence belongs to the theme, not the effect.
|
|
@@ -194,9 +194,9 @@ export function hydrateSketchStyle(raw: unknown): SketchStyle {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/** The second pass used to sit at a distance derived from the stroke width,
|
|
197
|
-
with no dial of its own. A
|
|
197
|
+
with no dial of its own. A sketchstyle stored before the dial comes back at that
|
|
198
198
|
distance rather than at whatever the fallback sketchstyle happens to carry. */
|
|
199
|
-
function restoreDerivedRetrace(stored: Record<string, unknown>, out:
|
|
199
|
+
function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
200
200
|
if (stored.retraceOffset === undefined) {
|
|
201
201
|
out.retraceOffset = Number(Math.max(1.2, out.strokeWidth * 0.55).toFixed(2));
|
|
202
202
|
}
|
|
@@ -204,7 +204,7 @@ function restoreDerivedRetrace(stored: Record<string, unknown>, out: SketchStyle
|
|
|
204
204
|
|
|
205
205
|
/** The four displacement dials used to be stated as the map's own `scale`,
|
|
206
206
|
which is the full swing: the number on the dial was twice the furthest
|
|
207
|
-
anything actually moved. They are peak travel in px now, so a
|
|
207
|
+
anything actually moved. They are peak travel in px now, so a sketchstyle stored
|
|
208
208
|
under the old names comes back halved and renders identically. */
|
|
209
209
|
const SWING_DIALS = {
|
|
210
210
|
fillScale: 'fillTravel',
|
|
@@ -216,11 +216,11 @@ const SWING_DIALS = {
|
|
|
216
216
|
/** The pen wobble used to be stated as `frequency`, in cycles per px, which is
|
|
217
217
|
the number the filter wants and not one anybody can picture. It is a
|
|
218
218
|
wavelength in px now, the way the mask states its blobs. */
|
|
219
|
-
function convertCyclesToWavelength(stored: Record<string, unknown>, out:
|
|
219
|
+
function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
220
220
|
const cycles = stored.frequency;
|
|
221
221
|
if (typeof cycles === 'number' && cycles > 0) out.wobble = Math.round(1 / cycles);
|
|
222
222
|
// The layer count was `octaves`, and its dial ran to 5. The top two moved
|
|
223
|
-
// nothing, so a
|
|
223
|
+
// nothing, so a sketchstyle stored there comes back at the roughest that reads.
|
|
224
224
|
// Both were multipliers on the frequency, so they ran the other way.
|
|
225
225
|
for (const [legacy, key] of [
|
|
226
226
|
['borderFrequency', 'borderWavelength'], ['iconFrequency', 'iconWavelength'],
|
|
@@ -234,9 +234,9 @@ function convertCyclesToWavelength(stored: Record<string, unknown>, out: SketchS
|
|
|
234
234
|
if (typeof layers === 'number') out.roughness = Math.min(3, Math.max(1, layers));
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
/** The blob size was one number for both axes. A
|
|
238
|
-
comes back square, with the two dials linked, which is the
|
|
239
|
-
function splitBlobAxes(stored: Record<string, unknown>, out:
|
|
237
|
+
/** The blob size was one number for both axes. A sketchstyle stored before the split
|
|
238
|
+
comes back square, with the two dials linked, which is the shape it had. */
|
|
239
|
+
function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
240
240
|
const blob = stored.maskBlob;
|
|
241
241
|
if (typeof blob !== 'number') return;
|
|
242
242
|
out.maskBlobX = blob;
|
|
@@ -249,13 +249,13 @@ function splitBlobAxes(stored: Record<string, unknown>, out: SketchStyle): void
|
|
|
249
249
|
of the field, so it came out either untouched or gone. It is a share of the
|
|
250
250
|
glyph now, and the old default reads as one period across the glyph, which
|
|
251
251
|
is what that size was aiming at. */
|
|
252
|
-
function convertIconTileToScale(stored: Record<string, unknown>, out:
|
|
252
|
+
function convertIconTileToScale(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
253
253
|
const tile = stored.iconMaskTile;
|
|
254
254
|
if (typeof tile !== 'number') return;
|
|
255
255
|
out.iconMaskScale = Math.min(5, Math.max(0.5, Number((tile / 90).toFixed(2))));
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
function halveSwingDials(stored: Record<string, unknown>, out:
|
|
258
|
+
function halveSwingDials(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
259
259
|
for (const [legacy, key] of Object.entries(SWING_DIALS)) {
|
|
260
260
|
const value = stored[legacy];
|
|
261
261
|
if (typeof value === 'number') out[key as 'fillTravel'] = value / 2;
|
|
@@ -265,7 +265,7 @@ function halveSwingDials(stored: Record<string, unknown>, out: SketchStyle): voi
|
|
|
265
265
|
/** The mask used to be a 600-unit tile painted at `maskScale` px, with the
|
|
266
266
|
coverage point buried in the hardness slope. Recover page-px blobs and
|
|
267
267
|
softness, and the levels the old slope and floor put the edge at. */
|
|
268
|
-
function convertTiledMask(stored: Record<string, unknown>, out:
|
|
268
|
+
function convertTiledMask(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
269
269
|
const scale = stored.maskScale;
|
|
270
270
|
if (typeof scale !== 'number') return;
|
|
271
271
|
const freq = stored.maskFrequency;
|
|
@@ -283,7 +283,7 @@ function convertTiledMask(stored: Record<string, unknown>, out: SketchStyle): vo
|
|
|
283
283
|
* levels the pair put the edge between, and rescale them onto the field as it
|
|
284
284
|
* is now: stretched onto its full range before the levels see it.
|
|
285
285
|
*/
|
|
286
|
-
function convertCutToLevels(stored: Record<string, unknown>, out:
|
|
286
|
+
function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
287
287
|
const contrast = stored.maskContrast;
|
|
288
288
|
const coverage = stored.maskCoverage;
|
|
289
289
|
if (typeof contrast !== 'number' || typeof coverage !== 'number') return;
|
|
@@ -297,9 +297,9 @@ function convertCutToLevels(stored: Record<string, unknown>, out: SketchStyle):
|
|
|
297
297
|
|
|
298
298
|
/** The pair were input levels, a cut through the field, and are output levels
|
|
299
299
|
now, the palest and densest the fill gets. The same two numbers carry over:
|
|
300
|
-
a
|
|
300
|
+
a sketchstyle cut between 20% and 50% comes back as a wash between 20% and 50% ink,
|
|
301
301
|
which keeps its spread and loses only the hole. */
|
|
302
|
-
function carryLevelsToOutput(stored: Record<string, unknown>, out:
|
|
302
|
+
function carryLevelsToOutput(stored: Record<string, unknown>, out: SketchStyleSettings): void {
|
|
303
303
|
if (typeof stored.maskLevelMin === 'number') out.maskOutputMin = stored.maskLevelMin;
|
|
304
304
|
if (typeof stored.maskLevelMax === 'number') out.maskOutputMax = stored.maskLevelMax;
|
|
305
305
|
}
|
|
@@ -65,7 +65,7 @@ export const sanitizeFileName = sanitizeFileNameImpl;
|
|
|
65
65
|
|
|
66
66
|
/** Flush the editor state to the buffer under `displayName`, the name of the
|
|
67
67
|
* theme the buffer belongs to, and clear the dirty flag. A capture reads the
|
|
68
|
-
* buffer back, so this is what makes Save and Adopt mean the
|
|
68
|
+
* buffer back, so this is what makes Save and Adopt mean the theme on screen. */
|
|
69
69
|
export async function persistColorsAndType(
|
|
70
70
|
state: EditorState,
|
|
71
71
|
displayName: string,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ThemeMeta, ColorsAndTypeMeta } from './themeTypes';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The one Load list. A theme is the whole
|
|
4
|
+
* The one Load list. A theme is the whole theme; a colors and type file is a
|
|
5
5
|
* preset holding that half of one. Both belong in the same window, told apart
|
|
6
6
|
* by a badge rather than by living in separate managers.
|
|
7
7
|
*
|
|
@@ -10,7 +10,7 @@ import type { ThemeMeta, ColorsAndTypeMeta } from './themeTypes';
|
|
|
10
10
|
* that copy is exactly the file the list must keep reachable.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export type LoadRowKind = '
|
|
13
|
+
export type LoadRowKind = 'theme' | 'layer';
|
|
14
14
|
|
|
15
15
|
export interface LoadRow {
|
|
16
16
|
/** `<kind>:<slug>`. The two kinds are separate resources with separate name
|
|
@@ -25,12 +25,12 @@ export interface LoadRow {
|
|
|
25
25
|
|
|
26
26
|
export const loadRowId = (kind: LoadRowKind, slug: string): string => `${kind}:${slug}`;
|
|
27
27
|
|
|
28
|
-
export function buildLoadRows(
|
|
29
|
-
const
|
|
28
|
+
export function buildLoadRows(themes: ThemeMeta[], layers: ColorsAndTypeMeta[]): LoadRow[] {
|
|
29
|
+
const themeRows: LoadRow[] = themes
|
|
30
30
|
.map((f): LoadRow => ({
|
|
31
|
-
fileName: loadRowId('
|
|
31
|
+
fileName: loadRowId('theme', f.fileName),
|
|
32
32
|
slug: f.fileName,
|
|
33
|
-
kind: '
|
|
33
|
+
kind: 'theme',
|
|
34
34
|
name: f.name,
|
|
35
35
|
updatedAt: f.updatedAt,
|
|
36
36
|
isProtected: f.isProtected,
|
|
@@ -46,12 +46,12 @@ export function buildLoadRows(looks: ThemeMeta[], layers: ColorsAndTypeMeta[]):
|
|
|
46
46
|
updatedAt: f.updatedAt,
|
|
47
47
|
isProtected: false,
|
|
48
48
|
}));
|
|
49
|
-
return [...
|
|
49
|
+
return [...themeRows, ...layerRows];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Whether picking this row loads colors and type alone. A layer file holds
|
|
54
|
-
* nothing else, so it ignores the toggle; a
|
|
54
|
+
* nothing else, so it ignores the toggle; a theme honors it.
|
|
55
55
|
*/
|
|
56
56
|
export function isColorsOnly(row: LoadRow | null, colorsOnly: boolean): boolean {
|
|
57
57
|
if (row?.kind === 'layer') return true;
|