@motion-proto/live-tokens 0.67.1 → 0.69.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-build-page/SKILL.md +16 -5
- package/.claude/skills/live-tokens-create-component/SKILL.md +43 -8
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +30 -1
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +131 -0
- package/.claude/skills/live-tokens-pick-component/SKILL.md +2 -1
- package/CHANGELOG.md +239 -0
- package/README.md +11 -4
- package/bin/check-component.mjs +367 -63
- package/bin/check-page.mjs +409 -0
- package/bin/cli.mjs +57 -8
- package/bin/generate-theme.mjs +5 -4
- package/bin/lib/cssValues.mjs +50 -0
- package/bin/lib/findings.mjs +106 -0
- package/bin/lib/tokenVocabulary.mjs +213 -0
- package/dist-plugin/adjust/index.cjs +174 -23
- package/dist-plugin/adjust/index.d.cts +1 -1
- package/dist-plugin/adjust/index.d.ts +1 -1
- package/dist-plugin/adjust/index.js +68 -23
- package/dist-plugin/{chunk-J2JT4UEA.js → chunk-2YNERPXY.js} +26 -26
- package/dist-plugin/{chunk-NE6N66EE.js → chunk-GPIBU44G.js} +107 -1
- package/dist-plugin/{chunk-ZHPX7ZYQ.js → chunk-RFVYPNRO.js} +39 -1
- package/dist-plugin/{dataPaths-DZUzVv8H.d.ts → dataPaths-bJTCEO4H.d.cts} +1 -1
- package/dist-plugin/{dataPaths-DZUzVv8H.d.cts → 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.cjs +107 -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 +173 -29
- package/dist-plugin/index.d.cts +1 -1
- package/dist-plugin/index.d.ts +1 -1
- package/dist-plugin/index.js +7 -7
- package/dist-plugin/migrateData/index.cjs +132 -26
- package/dist-plugin/migrateData/index.d.cts +1 -1
- package/dist-plugin/migrateData/index.d.ts +1 -1
- package/dist-plugin/migrateData/index.js +2 -2
- 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 +1 -1
- package/package.json +6 -5
- package/src/app/site.css +4 -4
- package/src/editor/bootstrap.ts +6 -6
- package/src/editor/component-editor/ButtonEditor.svelte +71 -4
- package/src/editor/component-editor/CardEditor.svelte +6 -6
- package/src/editor/component-editor/DialogEditor.svelte +3 -3
- package/src/editor/component-editor/IconButtonEditor.svelte +68 -3
- package/src/editor/component-editor/ImageEditor.svelte +8 -8
- package/src/editor/component-editor/ImageLightboxEditor.svelte +13 -2
- package/src/editor/component-editor/MenuSelectEditor.svelte +63 -3
- package/src/editor/component-editor/SegmentedControlEditor.svelte +63 -3
- package/src/editor/component-editor/SideNavigationEditor.svelte +67 -3
- package/src/editor/component-editor/SliderEditor.svelte +186 -0
- package/src/editor/component-editor/TabBarEditor.svelte +63 -3
- package/src/editor/component-editor/registry.ts +10 -0
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +4 -4
- package/src/editor/core/components/aliasKinds.ts +51 -28
- 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 +25 -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/store/editorPersistence.ts +44 -1
- package/src/editor/core/store/editorRenderer.ts +2 -2
- package/src/editor/core/store/editorStore.ts +18 -18
- package/src/editor/core/store/editorTypes.ts +7 -6
- package/src/editor/core/themes/colorsAndTypeService.ts +1 -1
- package/src/editor/core/themes/loadRows.ts +8 -8
- package/src/editor/core/themes/migrations/2026-09-01-gate-suffix-enabled.ts +31 -0
- package/src/editor/core/themes/migrations/2026-09-01-scrim-rename.ts +52 -0
- package/src/editor/core/themes/migrations/2026-09-01-tabbar-active-tint.ts +27 -0
- package/src/editor/core/themes/migrations/2026-09-01-tint-rename.ts +42 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/slices/domainVars.ts +2 -2
- package/src/editor/core/themes/slices/washes.ts +107 -0
- 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/editing-tokens.md +5 -3
- 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 +5 -5
- package/src/editor/index.ts +2 -2
- package/src/editor/pages/EditorShell.svelte +1 -1
- package/src/editor/ui/SurfacesTab.svelte +3 -3
- package/src/editor/ui/ThemePanel.svelte +52 -52
- package/src/editor/ui/UITokenSelector.svelte +1 -0
- package/src/editor/ui/VariablesTab.svelte +2 -2
- package/src/editor/ui/sections/{OverlaysSection.svelte → WashesSection.svelte} +44 -43
- package/src/editor/ui/sketch/SketchPreview.svelte +2 -2
- package/src/editor/ui/sketch/SketchTab.svelte +64 -38
- package/src/live-tokens/data/colors-and-type/autumn.json +6 -6
- package/src/live-tokens/data/colors-and-type/default.json +6 -6
- package/src/live-tokens/data/colors-and-type/halloween.json +6 -6
- package/src/live-tokens/data/colors-and-type/midnight-study.json +6 -6
- package/src/live-tokens/data/colors-and-type/ocean.json +6 -6
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +6 -6
- package/src/live-tokens/data/colors-and-type/sketchy.json +6 -6
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +6 -6
- package/src/live-tokens/data/colors-and-type/sunset.json +6 -6
- 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/themes/autumn.json +82 -14
- package/src/live-tokens/data/themes/halloween.json +82 -14
- package/src/live-tokens/data/themes/midnight-study.json +82 -14
- package/src/live-tokens/data/themes/ocean.json +82 -14
- package/src/live-tokens/data/themes/royal-velvet.json +82 -14
- package/src/live-tokens/data/themes/sketchy.json +82 -14
- package/src/live-tokens/data/themes/spring-meadow.json +82 -14
- package/src/live-tokens/data/themes/sunset.json +82 -14
- package/src/live-tokens/data/tokens.generated.css +6 -6
- package/src/system/backdrop/backdrop.ts +1 -1
- package/src/system/components/Button.svelte +28 -10
- package/src/system/components/Card.svelte +6 -6
- package/src/system/components/Dialog.svelte +3 -3
- package/src/system/components/IconButton.svelte +23 -7
- package/src/system/components/Image.svelte +6 -6
- package/src/system/components/MenuSelect.svelte +17 -1
- package/src/system/components/SectionDivider.svelte +1 -1
- package/src/system/components/SegmentedControl.svelte +20 -2
- package/src/system/components/SideNavigation.svelte +22 -2
- package/src/system/components/Slider.svelte +348 -0
- package/src/system/components/TabBar.svelte +20 -2
- package/src/system/styles/CONVENTIONS.md +2 -2
- package/src/system/styles/tokens.css +12 -4
- package/template/package.json +3 -2
- package/template/src/pages/Home.svelte +1 -1
- package/src/editor/core/themes/slices/overlays.ts +0 -101
|
@@ -131,10 +131,10 @@ export function resolveFontStackValues(
|
|
|
131
131
|
* in an iframe) via the same pipeline used for color variables.
|
|
132
132
|
*
|
|
133
133
|
* Returns the variables it set, so a caller tracking what it has applied — to
|
|
134
|
-
* tear those vars down when switching
|
|
134
|
+
* tear those vars down when switching themes — can record them without keeping
|
|
135
135
|
* its own copy of the stack list. A hand-maintained copy silently falls behind
|
|
136
136
|
* whenever a stack is added here, leaving the missed variable stuck at the
|
|
137
|
-
* outgoing
|
|
137
|
+
* outgoing theme's value; `--font-editorial` did exactly that.
|
|
138
138
|
*/
|
|
139
139
|
export function applyFontStacks(
|
|
140
140
|
stacks: FontStack[],
|
|
@@ -9,11 +9,11 @@ import { loadTheme } from '../themes/themeService';
|
|
|
9
9
|
import { previewSketchStyle, revertSketchStylePreview } from '../sketch/sketchStore';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Client-side rendering of a saved file, for browsing
|
|
13
|
-
* one. A theme previews as a whole
|
|
12
|
+
* Client-side rendering of a saved file, for browsing themes without committing
|
|
13
|
+
* one. A theme previews as a whole next; a colors-and-type file previews as
|
|
14
14
|
* colors and type over the components the user has right now. Nothing here writes to the server,
|
|
15
15
|
* mutates the editor store, or marks anything dirty: a preview is paint only.
|
|
16
|
-
* Save-As stays honest because `
|
|
16
|
+
* Save-As stays honest because `captureThemeContent` reads the server's active files,
|
|
17
17
|
* which a preview never touches.
|
|
18
18
|
*
|
|
19
19
|
* Both painting and reverting run the same derivation the renderer runs on
|
|
@@ -27,23 +27,23 @@ import { previewSketchStyle, revertSketchStylePreview } from '../sketch/sketchSt
|
|
|
27
27
|
* CSS vars do.
|
|
28
28
|
*
|
|
29
29
|
* One preview is live at a time, whichever kind it is: painting reverts first,
|
|
30
|
-
* so every
|
|
30
|
+
* so every next is a diff against the user's real state.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
/** Everything a
|
|
33
|
+
/** Everything a next puts on the page: the full var set (colors and type,
|
|
34
34
|
* components and the composed `--font-*` stacks) plus the sources its faces
|
|
35
35
|
* come from. */
|
|
36
|
-
export interface
|
|
36
|
+
export interface RenderedTheme {
|
|
37
37
|
vars: Record<string, string>;
|
|
38
38
|
fontSources: FontSource[];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The full var set a theme paints. Components the theme carries no config
|
|
43
|
-
* for render `defaults`' config, matching Apply's complete-
|
|
43
|
+
* for render `defaults`' config, matching Apply's complete-next semantics;
|
|
44
44
|
* configs for components this install lacks are skipped, as Apply skips them.
|
|
45
45
|
*/
|
|
46
|
-
export function
|
|
46
|
+
export function renderTheme(theme: Theme, defaults: Theme): RenderedTheme {
|
|
47
47
|
const colorsAndType = structuredClone(theme.colorsAndType);
|
|
48
48
|
migrateColorsAndTypeFonts(colorsAndType);
|
|
49
49
|
const state = colorsAndTypeToState(colorsAndType);
|
|
@@ -60,27 +60,27 @@ export function themeLook(theme: Theme, defaults: Theme): RenderedLook {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* The full var set colors and type paint on their own. They are not a whole
|
|
63
|
+
* The full var set colors and type paint on their own. They are not a whole next,
|
|
64
64
|
* so the components stay as the user has them: `colorsAndTypeToState` carries the live
|
|
65
65
|
* component slice forward (`loadComponentsFromVars`) and strips component-owned
|
|
66
66
|
* vars out of their own bag, which is exactly the composition a preview
|
|
67
67
|
* wants. No defaults fetch, no component reset.
|
|
68
68
|
*/
|
|
69
|
-
export function
|
|
69
|
+
export function renderColorsAndType(colorsAndType: ColorsAndType): RenderedTheme {
|
|
70
70
|
const next = structuredClone(colorsAndType);
|
|
71
71
|
migrateColorsAndTypeFonts(next);
|
|
72
72
|
const vars = deriveCssVars(colorsAndTypeToState(next));
|
|
73
73
|
return { vars, fontSources: next.fontSources ?? [] };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/** The
|
|
77
|
-
export function
|
|
76
|
+
/** The next the editor store currently describes — the state a revert returns to. */
|
|
77
|
+
export function liveTheme(): RenderedTheme {
|
|
78
78
|
const state = get(editorState);
|
|
79
79
|
const vars = deriveCssVars(state);
|
|
80
80
|
return { vars, fontSources: state.fonts.sources };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
let livePreview:
|
|
83
|
+
let livePreview: RenderedTheme | null = null;
|
|
84
84
|
let defaultsPromise: Promise<Theme> | null = null;
|
|
85
85
|
// Whether the current preview session has a sketchstyle painted over the live
|
|
86
86
|
// buffer, so a colors-only preview (which never previews sketch) and revert
|
|
@@ -101,7 +101,7 @@ function loadDefaults(): Promise<Theme> {
|
|
|
101
101
|
return defaultsPromise;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function paint(next:
|
|
104
|
+
function paint(next: RenderedTheme, from: RenderedTheme): void {
|
|
105
105
|
batchCssVarChanges(() => {
|
|
106
106
|
for (const [name, value] of Object.entries(next.vars)) {
|
|
107
107
|
if (from.vars[name] !== value) setCssVar(name, value);
|
|
@@ -114,33 +114,33 @@ function paint(next: RenderedLook, from: RenderedLook): void {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Show `
|
|
117
|
+
* Show `next` on the page. Re-entrant previews diff directly from the next
|
|
118
118
|
* already painted; only Cancel restores the live store projection. This avoids
|
|
119
119
|
* repainting the live theme as an invisible intermediate on every picker row.
|
|
120
120
|
*/
|
|
121
|
-
function applyPreview(
|
|
122
|
-
paint(
|
|
123
|
-
livePreview =
|
|
121
|
+
function applyPreview(next: RenderedTheme): void {
|
|
122
|
+
paint(next, livePreview ?? liveTheme());
|
|
123
|
+
livePreview = next;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/** Paint a whole
|
|
126
|
+
/** Paint a whole next: the theme's colors and type, every component config
|
|
127
127
|
* it carries, and its sketchstyle — present or not, since a theme with none
|
|
128
128
|
* paints crisp regardless of what is live (invariant 3). */
|
|
129
129
|
export async function previewTheme(theme: Theme): Promise<void> {
|
|
130
130
|
const gen = generation;
|
|
131
131
|
const defaults = await loadDefaults();
|
|
132
132
|
if (gen !== generation) return;
|
|
133
|
-
applyPreview(
|
|
134
|
-
previewSketchStyle(theme.
|
|
133
|
+
applyPreview(renderTheme(theme, defaults));
|
|
134
|
+
previewSketchStyle(theme.sketchSettings);
|
|
135
135
|
sketchPreviewActive = true;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/** Paint colors and type over the components as they stand. Not a whole
|
|
139
|
-
*
|
|
139
|
+
* next, so the sketchstyle stays live too — reverting a sketch preview a
|
|
140
140
|
* prior row left painted, if one is running. */
|
|
141
141
|
export function previewColorsAndType(colorsAndType: ColorsAndType): void {
|
|
142
142
|
generation++;
|
|
143
|
-
applyPreview(
|
|
143
|
+
applyPreview(renderColorsAndType(colorsAndType));
|
|
144
144
|
if (sketchPreviewActive) {
|
|
145
145
|
revertSketchStylePreview();
|
|
146
146
|
sketchPreviewActive = false;
|
|
@@ -151,7 +151,7 @@ export function previewColorsAndType(colorsAndType: ColorsAndType): void {
|
|
|
151
151
|
export function revertPreview(): void {
|
|
152
152
|
generation++;
|
|
153
153
|
if (!livePreview) return;
|
|
154
|
-
paint(
|
|
154
|
+
paint(liveTheme(), livePreview);
|
|
155
155
|
livePreview = null;
|
|
156
156
|
if (sketchPreviewActive) {
|
|
157
157
|
revertSketchStylePreview();
|
|
@@ -160,11 +160,11 @@ export function revertPreview(): void {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/** Release the preview without repainting. The caller must immediately load
|
|
163
|
-
* the exact
|
|
164
|
-
* selected theme is already on screen, so restoring the old live
|
|
163
|
+
* the exact next being previewed into the store. This is the Save handoff: the
|
|
164
|
+
* selected theme is already on screen, so restoring the old live next before
|
|
165
165
|
* applying it would add work and create a visible flash across the request.
|
|
166
166
|
* The sketchstyle stays painted for the same reason, though the handoff is not
|
|
167
|
-
* quite free: when the live state was crisp, `
|
|
167
|
+
* quite free: when the live state was crisp, `openThemeSketchSettings` writes the
|
|
168
168
|
* settings before the flag, so the sheet and the filter bank come down and go
|
|
169
169
|
* straight back up in between. That order is still the right one, since writing
|
|
170
170
|
* the flag first would paint the OLD dials for a frame. */
|
|
@@ -28,17 +28,17 @@ export function bumpComponentActiveRevision(): void {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* The live
|
|
31
|
+
* The live theme has moved past the last bake. Only Adopt bakes the CSS, so a
|
|
32
32
|
* buffer write or a theme save after one leaves production a version behind,
|
|
33
33
|
* and no pointer file records when the bake happened. Every surface reads the
|
|
34
34
|
* one signal: a component save has to reach the theme panel's Adopt, and a
|
|
35
35
|
* component editor's Adopt has to clear it for the panel.
|
|
36
36
|
*
|
|
37
|
-
* Set by the client writes that move the live
|
|
37
|
+
* Set by the client writes that move the live theme (`writeWorkingColorsAndType`,
|
|
38
38
|
* `writeWorkingComponentConfig`, `saveActiveTheme`, `saveAsTheme`, and every
|
|
39
39
|
* sketch gesture that changes what the effect paints: `setSketchEnabled`,
|
|
40
40
|
* `updateSketchSettings`, `selectSketchStyle`);
|
|
41
|
-
* cleared by `
|
|
41
|
+
* cleared by `adoptTheme` and by `hydrateAppliedTheme`, on this document and on
|
|
42
42
|
* every peer an Apply broadcasts to. Module-level, so it survives the
|
|
43
43
|
* remounts a view switch causes.
|
|
44
44
|
*/
|
|
@@ -1,70 +1,78 @@
|
|
|
1
1
|
import { derived, get, type Readable } from 'svelte/store';
|
|
2
2
|
import { THEME_SKETCH_ID } from './sketchStyles';
|
|
3
|
-
import {
|
|
3
|
+
import { sketchStyleById, sketchStyles, type SketchStyle } from './sketchRegistry';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
sameSketchStyle,
|
|
6
6
|
selectSketchStyle,
|
|
7
|
-
|
|
7
|
+
selectUnsavedSketchStyle,
|
|
8
8
|
setSketchEnabled,
|
|
9
9
|
sketchEnabled,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
selectedSketchStyleId,
|
|
11
|
+
themeSketchSettings,
|
|
12
12
|
} from './sketchStore';
|
|
13
13
|
|
|
14
|
-
export {
|
|
15
|
-
export type {
|
|
14
|
+
export { sketchStyles, registerSketchStyle } from './sketchRegistry';
|
|
15
|
+
export type { SketchStyle, SketchStyleSource, RegisterSketchStyleInput } from './sketchRegistry';
|
|
16
16
|
|
|
17
|
-
/** A
|
|
17
|
+
/** A style as one picker row. The theme's own style is offered this way too, and
|
|
18
18
|
it belongs to no pool entry, so the fields only a pool entry has are off. */
|
|
19
|
-
export type
|
|
19
|
+
export type SketchStyleRow = Omit<SketchStyle, 'settings' | 'source'>;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Sketch settings the open theme carries that no sketchstyle file holds, as one
|
|
23
|
+
* more row for a picker: same shape as a shipped sketchstyle, and `setSketch`
|
|
24
|
+
* takes its id like any other. Null when the theme carries no settings, and
|
|
25
|
+
* null when what it carries matches a sketchstyle, since that file's own row
|
|
26
|
+
* already names it.
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* A theme embeds its sketch settings by value, so it can hold a set that
|
|
29
|
+
* belongs to no file: Save As is what turns one into a sketchstyle. Until then
|
|
30
|
+
* this is the only row that can offer it. Without it the settings are a one-way
|
|
31
|
+
* door, a visitor picks Pencil and nothing can take them back.
|
|
32
|
+
*
|
|
33
|
+
* Labelled "Unsaved" rather than by the settings' own label, which lies: a theme
|
|
34
|
+
* tuned off `marker` still carries the label "Marker", so a row built from it
|
|
35
|
+
* would sit beside the shipped Marker claiming to be it.
|
|
32
36
|
*/
|
|
33
|
-
export const
|
|
34
|
-
[
|
|
35
|
-
([
|
|
36
|
-
if (!
|
|
37
|
-
if (
|
|
38
|
-
return {
|
|
37
|
+
export const unsavedSketchStyle: Readable<SketchStyleRow | null> = derived(
|
|
38
|
+
[themeSketchSettings, sketchStyles],
|
|
39
|
+
([settings, styles]) => {
|
|
40
|
+
if (!settings) return null;
|
|
41
|
+
if (styles.some((style) => sameSketchStyle(style.settings, settings))) return null;
|
|
42
|
+
return {
|
|
43
|
+
id: THEME_SKETCH_ID,
|
|
44
|
+
label: 'Unsaved',
|
|
45
|
+
blurb: 'Sketch settings this theme carries that no sketchstyle holds.',
|
|
46
|
+
};
|
|
39
47
|
},
|
|
40
48
|
);
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
51
|
* What the page is drawing with. Three states, not two: the effect can be on
|
|
44
|
-
* under a
|
|
52
|
+
* under a style no shipped sketchstyle names — one saved to a file, or one a
|
|
45
53
|
* theme carried — and a picker that collapses that into `off` tells the
|
|
46
54
|
* visitor the page is crisp while it is visibly drawn.
|
|
47
55
|
*
|
|
48
|
-
* A dial moved off a shipped
|
|
49
|
-
* own rule: the pick says where the
|
|
56
|
+
* A dial moved off a shipped style keeps naming it, which is `selectSketchStyle`'s
|
|
57
|
+
* own rule: the pick says where the style came from, and `sketchDirty` says it
|
|
50
58
|
* has since drifted.
|
|
51
59
|
*/
|
|
52
60
|
export type SketchPick =
|
|
53
61
|
| { state: 'off' }
|
|
54
|
-
| { state: '
|
|
62
|
+
| { state: 'style'; style: SketchStyleRow }
|
|
55
63
|
| { state: 'adjusted' };
|
|
56
64
|
|
|
57
65
|
export const sketchPick: Readable<SketchPick> = derived(
|
|
58
|
-
[sketchEnabled,
|
|
59
|
-
([on, name,
|
|
66
|
+
[sketchEnabled, selectedSketchStyleId, sketchStyles, unsavedSketchStyle],
|
|
67
|
+
([on, name, styles, unsaved]): SketchPick => {
|
|
60
68
|
if (!on) return { state: 'off' };
|
|
61
|
-
const
|
|
62
|
-
return
|
|
69
|
+
const style = styles.find((s) => s.id === name) ?? (unsaved?.id === name ? unsaved : undefined);
|
|
70
|
+
return style ? { state: 'style', style: { id: style.id, label: style.label, blurb: style.blurb } } : { state: 'adjusted' };
|
|
63
71
|
},
|
|
64
72
|
);
|
|
65
73
|
|
|
66
74
|
/**
|
|
67
|
-
* Draw the page with one of the
|
|
75
|
+
* Draw the page with one of the styles in the pool, or `null` for none.
|
|
68
76
|
*
|
|
69
77
|
* The only supported way for a consumer to drive the effect. Reaching for
|
|
70
78
|
* `applySketchLayer` instead paints a stylesheet the store does not know it
|
|
@@ -77,15 +85,15 @@ export function setSketch(id: string | null): void {
|
|
|
77
85
|
return;
|
|
78
86
|
}
|
|
79
87
|
if (id === THEME_SKETCH_ID) {
|
|
80
|
-
if (!get(
|
|
81
|
-
throw new Error('No theme sketchstyle to draw with. `
|
|
88
|
+
if (!get(themeSketchSettings)) {
|
|
89
|
+
throw new Error('No theme sketchstyle to draw with. `unsavedSketchStyle` is null unless a theme carries one.');
|
|
82
90
|
}
|
|
83
|
-
|
|
91
|
+
selectUnsavedSketchStyle();
|
|
84
92
|
setSketchEnabled(true);
|
|
85
93
|
return;
|
|
86
94
|
}
|
|
87
|
-
if (!
|
|
88
|
-
throw new Error(`Unknown sketchstyle "${id}". Ids come from
|
|
95
|
+
if (!sketchStyleById(id)) {
|
|
96
|
+
throw new Error(`Unknown sketchstyle "${id}". Ids come from sketchStyles and unsavedSketchStyle.`);
|
|
89
97
|
}
|
|
90
98
|
selectSketchStyle(id);
|
|
91
99
|
setSketchEnabled(true);
|
|
@@ -106,22 +114,22 @@ export { hasPersistedSketchState } from './sketchStore';
|
|
|
106
114
|
* already decided for itself.
|
|
107
115
|
*
|
|
108
116
|
* The route from a saved theme to a built page. Hand it the theme's
|
|
109
|
-
* `
|
|
117
|
+
* `sketchSettings` field, raw: a built site has no theme API, so it reads its own
|
|
110
118
|
* theme JSON and this hydrates what it finds. Absent, `null`, or anything that
|
|
111
119
|
* is not an object all mean the same thing, which is no sketch.
|
|
112
120
|
*
|
|
113
121
|
* Call it before mounting, the way dev boot does (`bootstrap.ts` awaits
|
|
114
|
-
* `initializeTheme` first), so the
|
|
122
|
+
* `initializeTheme` first), so the style is up on the first frame rather than
|
|
115
123
|
* arriving over a crisp page.
|
|
116
124
|
*
|
|
117
125
|
* A visitor who has recorded a pick of their own keeps it, None included: this
|
|
118
126
|
* seeds an undecided browser and never overwrites a decided one, so it is safe
|
|
119
|
-
* to call on every boot. `
|
|
120
|
-
* can offer the theme's
|
|
127
|
+
* to call on every boot. `unsavedSketchStyle` is populated either way, so a picker
|
|
128
|
+
* can offer the theme's style as a row whether or not this painted it.
|
|
121
129
|
*/
|
|
122
130
|
export { seedSketchFromTheme } from './sketchStore';
|
|
123
131
|
|
|
124
|
-
/** The dial set a theme's `
|
|
132
|
+
/** The dial set a theme's `sketchSettings` field holds, for a consumer typing the
|
|
125
133
|
value it pulled out of its own theme JSON. `seedSketchFromTheme` takes it
|
|
126
134
|
raw, so nothing has to be cast to hand it over. */
|
|
127
|
-
export type {
|
|
135
|
+
export type { SketchStyleSettings } from './sketchStyles';
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Everything is integer maths on a Float32Array with no DOM, so what a test
|
|
20
20
|
* asserts is what the browser paints.
|
|
21
21
|
*/
|
|
22
|
-
import type {
|
|
22
|
+
import type { SketchStyleSettings } from './sketchStyles';
|
|
23
23
|
|
|
24
24
|
/** Samples across the tile, on both axes. Fixed, whatever the blobs measure:
|
|
25
25
|
the field is blobs and gradients, so the browser stretching the tile back to
|
|
@@ -69,7 +69,7 @@ export interface MaskLattice {
|
|
|
69
69
|
angle: number;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function maskLattice(s:
|
|
72
|
+
export function maskLattice(s: SketchStyleSettings): MaskLattice {
|
|
73
73
|
const ask = s.maskBlobX === s.maskBlobY ? 0 : s.maskAngle;
|
|
74
74
|
const rad = ask * (Math.PI / 180);
|
|
75
75
|
const cos = Math.cos(rad), sin = Math.sin(rad);
|
|
@@ -78,7 +78,7 @@ export function maskLattice(s: SketchStyle): MaskLattice {
|
|
|
78
78
|
const vx = fit(dx, ask, (v) => turnOf(-v[1] * s.maskBlobY, v[0] * s.maskBlobX));
|
|
79
79
|
let vy = fit(dy, ask, (v) => turnOf(v[0] * s.maskBlobX, v[1] * s.maskBlobY));
|
|
80
80
|
// Two vectors along one line describe no tile at all. The turn of the first
|
|
81
|
-
//
|
|
81
|
+
// fixes the tile, so the second is the one that gives way.
|
|
82
82
|
if (vx[0] * vy[1] - vx[1] * vy[0] === 0) vy = [-vx[1], vx[0]];
|
|
83
83
|
return {
|
|
84
84
|
vx,
|
|
@@ -131,7 +131,7 @@ function fit(d: Vec, ask: number, turn: (v: Vec) => number): Vec {
|
|
|
131
131
|
* fitted to it, which meant the dial could only reach the sizes that divide
|
|
132
132
|
* 600: it said 250px and painted 300px, and there was nothing above that.
|
|
133
133
|
*/
|
|
134
|
-
export function maskTile(s:
|
|
134
|
+
export function maskTile(s: SketchStyleSettings): { w: number; h: number } {
|
|
135
135
|
const { w, h } = maskLattice(s);
|
|
136
136
|
return { w, h };
|
|
137
137
|
}
|
|
@@ -228,11 +228,11 @@ function makePerlin(seed: number): (x: number, y: number, w: Wrap) => number {
|
|
|
228
228
|
* the one before, stretched onto its own measured range.
|
|
229
229
|
*
|
|
230
230
|
* `veined` folds each octave at zero before summing. The fold puts a crease
|
|
231
|
-
* wherever the octave crossed zero, which is the marbled
|
|
231
|
+
* wherever the octave crossed zero, which is the marbled effect; plain sum is
|
|
232
232
|
* cloud.
|
|
233
233
|
*/
|
|
234
234
|
function rawField(
|
|
235
|
-
s:
|
|
235
|
+
s: SketchStyleSettings, seed: number, raster: number, lattice: MaskLattice,
|
|
236
236
|
): Float32Array {
|
|
237
237
|
const noise = makePerlin(seed);
|
|
238
238
|
const veined = s.maskGrain === 'turbulence';
|
|
@@ -388,7 +388,7 @@ function boxPass(f: Float32Array, n: number, radius: number, stride: number): Fl
|
|
|
388
388
|
all four; the levels, the posterising and the blur are one pass each. */
|
|
389
389
|
let rawCache: { key: string; field: Float32Array } | null = null;
|
|
390
390
|
|
|
391
|
-
function cachedRaw(s:
|
|
391
|
+
function cachedRaw(s: SketchStyleSettings, seed: number): Float32Array {
|
|
392
392
|
const lattice = maskLattice(s);
|
|
393
393
|
// Keyed on the tile vectors rather than the blob sizes: most of the Scale
|
|
394
394
|
// dial's travel paints the same lattice at another size, and the field is
|
|
@@ -407,7 +407,7 @@ function cachedRaw(s: SketchStyle, seed: number): Float32Array {
|
|
|
407
407
|
* pass did.
|
|
408
408
|
*/
|
|
409
409
|
export function buildMaskField(
|
|
410
|
-
s:
|
|
410
|
+
s: SketchStyleSettings, seed = 9, through?: MaskStage,
|
|
411
411
|
): { field: Float32Array; raster: number } {
|
|
412
412
|
const raw = cachedRaw(s, seed);
|
|
413
413
|
if (through === 'noise') return { field: raw, raster: RASTER };
|
|
@@ -544,7 +544,7 @@ const CACHE_MAX = 6;
|
|
|
544
544
|
const cache = new Map<string, string>();
|
|
545
545
|
|
|
546
546
|
/** The field as a `url(...)` for `mask-image`. */
|
|
547
|
-
export function buildMaskUri(s:
|
|
547
|
+
export function buildMaskUri(s: SketchStyleSettings, seed = 9, through?: MaskStage): string {
|
|
548
548
|
const key = [...KEYS.map((k) => s[k]), seed, through ?? 'all'].join('|');
|
|
549
549
|
const hit = cache.get(key);
|
|
550
550
|
if (hit) return hit;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sketch effect layer.
|
|
3
3
|
*
|
|
4
|
-
* Builds an SVG filter bank and a stylesheet from one
|
|
4
|
+
* Builds an SVG filter bank and a stylesheet from one SketchStyleSettings and
|
|
5
5
|
* injects both into every document cssVarSync tracks, so the host page behind
|
|
6
6
|
* the overlay iframe gets the same effect the editor's preview shows.
|
|
7
7
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { getSyncedDocuments } from '../cssVarSync';
|
|
18
18
|
import { buildMaskUri, maskTile } from './maskField';
|
|
19
|
-
import type {
|
|
19
|
+
import type { SketchStyleSettings } from './sketchStyles';
|
|
20
20
|
|
|
21
21
|
const DEFS_ATTR = 'data-sketch-defs';
|
|
22
22
|
const STYLE_ATTR = 'data-sketch-style';
|
|
@@ -227,6 +227,22 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
227
227
|
// Holds the picture inside the frame; without it a zoom spills out square.
|
|
228
228
|
clips: true,
|
|
229
229
|
},
|
|
230
|
+
...['single', 'range'].map((v) => ({
|
|
231
|
+
sel: `.slider.${v} .slider-track`,
|
|
232
|
+
fill: `var(--slider-${v}-track-surface)`,
|
|
233
|
+
stroke: `var(--slider-${v}-track-border)`,
|
|
234
|
+
radius: `var(--slider-${v}-track-radius, 0px)`,
|
|
235
|
+
})),
|
|
236
|
+
// The fill rides inside the track, so it is stroked by nothing and hatched
|
|
237
|
+
// in the track's ink.
|
|
238
|
+
...['single', 'range'].map((v) => ({
|
|
239
|
+
sel: `.slider.${v} .slider-fill`,
|
|
240
|
+
fill: `var(--slider-${v}-fill)`,
|
|
241
|
+
stroke: 'transparent',
|
|
242
|
+
hatch: `var(--slider-${v}-track-border)`,
|
|
243
|
+
radius: `var(--slider-${v}-track-radius, 0px)`,
|
|
244
|
+
positioned: true,
|
|
245
|
+
})),
|
|
230
246
|
{ sel: '.toggle .track', stem: 'toggle-track' },
|
|
231
247
|
{ sel: '.toggle.on .track', stem: 'toggle-on-track', radius: 'var(--toggle-track-radius, 0px)' },
|
|
232
248
|
|
|
@@ -325,7 +341,7 @@ const UNCLIPPED = PART_SPECS.filter((p) => !p.clips).map((p) => p.sel).join(', '
|
|
|
325
341
|
const CLIPPED = PART_SPECS.filter((p) => p.clips).map((p) => p.sel).join(', ');
|
|
326
342
|
const UNMASKED = PART_SPECS.filter((p) => p.unmasked).map((p) => p.sel).join(', ');
|
|
327
343
|
|
|
328
|
-
export function buildDefsMarkup(s:
|
|
344
|
+
export function buildDefsMarkup(s: SketchStyleSettings): string {
|
|
329
345
|
/**
|
|
330
346
|
* `warp` is the shape stage: one wave of noise whose wavelength spans a whole
|
|
331
347
|
* component, so the four corners sample different parts of the field and the
|
|
@@ -559,7 +575,7 @@ const MASK_POS = ['0 0', '-137px -211px', '-311px -97px', '-73px -389px', '-419p
|
|
|
559
575
|
* A hand holds a rough size and misses it by a bit each time, so half to full
|
|
560
576
|
* is the shape of the error. The four land in a different order row to row.
|
|
561
577
|
*
|
|
562
|
-
* Added, never subtracted, because a hand-drawn
|
|
578
|
+
* Added, never subtracted, because a hand-drawn line tends to sit on a theme
|
|
563
579
|
* whose corners are already tight: a coefficient set that swung both ways
|
|
564
580
|
* spent half its range clamped at zero.
|
|
565
581
|
*/
|
|
@@ -584,7 +600,7 @@ const swing = (travel: number) => String(Number((travel * 2).toFixed(4)));
|
|
|
584
600
|
/** Squares the displacement wave off around 0.5, its zero, so full amplitude is
|
|
585
601
|
spent along the whole edge rather than only where the wave peaks. Both
|
|
586
602
|
channels take it: the map reads x from R and y from G. */
|
|
587
|
-
function squareOff(s:
|
|
603
|
+
function squareOff(s: SketchStyleSettings, from: string, to: string): string {
|
|
588
604
|
if (s.waveform <= 1) return '';
|
|
589
605
|
const slope = s.waveform.toFixed(2);
|
|
590
606
|
const intercept = ((1 - s.waveform) / 2).toFixed(3);
|
|
@@ -594,7 +610,7 @@ function squareOff(s: SketchStyle, from: string, to: string): string {
|
|
|
594
610
|
`</feComponentTransfer>`;
|
|
595
611
|
}
|
|
596
612
|
|
|
597
|
-
const squaredResult = (s:
|
|
613
|
+
const squaredResult = (s: SketchStyleSettings, from: string, to: string) =>
|
|
598
614
|
(s.waveform > 1 ? to : from);
|
|
599
615
|
|
|
600
616
|
/** Along-stroke pressure wavelength. Low, with a high floor in the transfer
|
|
@@ -650,7 +666,7 @@ const RETRACE_SEED = 53;
|
|
|
650
666
|
const WARP_FREQUENCY = 0.08;
|
|
651
667
|
|
|
652
668
|
|
|
653
|
-
export function buildStylesheet(s:
|
|
669
|
+
export function buildStylesheet(s: SketchStyleSettings): string {
|
|
654
670
|
const on = '[data-sketch]';
|
|
655
671
|
const parts = `:is(${PARTS})`;
|
|
656
672
|
const el = `${on} ${parts}`;
|
|
@@ -1089,7 +1105,7 @@ function defsNode(doc: Document): SVGSVGElement {
|
|
|
1089
1105
|
* This only makes the effect *available*. An element opts in by carrying
|
|
1090
1106
|
* data-sketch, which is what `setSketchScope` writes.
|
|
1091
1107
|
*/
|
|
1092
|
-
export function applySketchLayer(settings:
|
|
1108
|
+
export function applySketchLayer(settings: SketchStyleSettings): void {
|
|
1093
1109
|
const defs = buildDefsMarkup(settings);
|
|
1094
1110
|
const css = buildStylesheet(settings);
|
|
1095
1111
|
for (const doc of getSyncedDocuments()) {
|
|
@@ -1132,7 +1148,7 @@ export function removeSketchLayer(): void {
|
|
|
1132
1148
|
* The host page's root and the editor's own preview container are both scopes,
|
|
1133
1149
|
* which is why this takes an element rather than assuming documentElement.
|
|
1134
1150
|
*/
|
|
1135
|
-
export function setSketchScope(el: HTMLElement | null, settings:
|
|
1151
|
+
export function setSketchScope(el: HTMLElement | null, settings: SketchStyleSettings | null): void {
|
|
1136
1152
|
if (!el) return;
|
|
1137
1153
|
if (!settings) {
|
|
1138
1154
|
el.removeAttribute('data-sketch');
|