@motion-proto/live-tokens 0.61.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 +114 -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-OIOXU7FR.js → chunk-ZHPX7ZYQ.js} +83 -25
- 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 +461 -58
- 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 +83 -25
- 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 +32 -0
- package/src/editor/component-editor/CardEditor.svelte +7 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +25 -2
- package/src/editor/core/preview/lookPreview.ts +46 -5
- package/src/editor/core/productionPulse.ts +6 -2
- package/src/editor/core/sketch/maskField.ts +75 -39
- package/src/editor/core/sketch/sketchLayer.ts +32 -11
- package/src/editor/core/sketch/sketchStore.ts +311 -86
- package/src/editor/core/sketch/{sketchPresetService.ts → sketchStyleService.ts} +14 -14
- package/src/editor/core/sketch/{sketchPresets.ts → sketchStyles.ts} +27 -28
- 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 +34 -18
- 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 +8 -0
- package/src/editor/ui/EditorViewSwitcher.svelte +3 -3
- package/src/editor/ui/ThemePanel.svelte +47 -1
- package/src/editor/ui/sections/textStyles.ts +29 -1
- package/src/editor/ui/sketch/SketchPreview.svelte +3 -3
- package/src/editor/ui/sketch/SketchTab.svelte +120 -134
- package/src/live-tokens/data/colors-and-type/midnight-study.json +40 -30
- package/src/live-tokens/data/themes/autumn.json +3 -3
- package/src/live-tokens/data/themes/halloween.json +3 -3
- package/src/live-tokens/data/themes/midnight-study.json +61 -49
- package/src/live-tokens/data/themes/ocean.json +3 -3
- package/src/live-tokens/data/themes/royal-velvet.json +3 -3
- package/src/live-tokens/data/themes/sketchy.json +3 -3
- package/src/live-tokens/data/themes/spring-meadow.json +3 -3
- package/src/live-tokens/data/themes/sunset.json +3 -3
- package/src/system/components/Card.svelte +27 -9
- package/src/system/components/FloatingTokenTags.css +10 -8
- package/src/system/components/ImageLightbox.svelte +5 -2
- package/src/system/components/SectionDivider.svelte +3 -3
- package/src/system/components/SegmentedControl.svelte +11 -9
- package/src/system/styles/tokens.css +28 -5
- package/template/src/pages/Home.svelte +1 -11
|
@@ -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 SketchStyle 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, MASK_TILE } from './maskField';
|
|
19
|
-
import type {
|
|
19
|
+
import type { SketchStyle } from './sketchStyles';
|
|
20
20
|
|
|
21
21
|
const DEFS_ATTR = 'data-sketch-defs';
|
|
22
22
|
const STYLE_ATTR = 'data-sketch-style';
|
|
@@ -84,6 +84,12 @@ interface PartSpec {
|
|
|
84
84
|
drawn corner radii on the host instead, so the clip at least turns the
|
|
85
85
|
same way the ink does. */
|
|
86
86
|
clips?: boolean;
|
|
87
|
+
/** Keeps the ink coverage mask off the fill. Coverage wears a fill through in
|
|
88
|
+
patches, and a part that floats over arbitrary page content shows whatever
|
|
89
|
+
is behind it through the worn places: it stops reading as a surface the
|
|
90
|
+
pointer can land on. Set it where the part floats over the page rather
|
|
91
|
+
than over a scrim of its own. */
|
|
92
|
+
unmasked?: boolean;
|
|
87
93
|
positioned?: boolean;
|
|
88
94
|
strokeless?: boolean;
|
|
89
95
|
}
|
|
@@ -165,7 +171,7 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
165
171
|
clips: true,
|
|
166
172
|
},
|
|
167
173
|
// The arrow is the tooltip's own ::after, so the box takes the fill only.
|
|
168
|
-
{ sel: '.tooltip', stem: 'tooltip', positioned: true, strokeless: true },
|
|
174
|
+
{ sel: '.tooltip', stem: 'tooltip', positioned: true, strokeless: true, unmasked: true },
|
|
169
175
|
|
|
170
176
|
// Status blocks
|
|
171
177
|
...STATUS_VARIANTS.map((v) => ({ sel: `.callout-${v}`, stem: `callout-${v}` })),
|
|
@@ -189,6 +195,7 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
189
195
|
stroke: 'var(--menuselect-menu-border)',
|
|
190
196
|
radius: 'var(--menuselect-menu-radius, 0px)',
|
|
191
197
|
shadow: 'var(--menuselect-menu-shadow, none)',
|
|
198
|
+
unmasked: true,
|
|
192
199
|
},
|
|
193
200
|
{ sel: '.tab', stem: 'tabbar-default', radius: 'var(--tabbar-default-tab-top-radius, 0px)' },
|
|
194
201
|
{ sel: '.tab.active', stem: 'tabbar-active', radius: 'var(--tabbar-active-tab-top-radius, 0px)' },
|
|
@@ -316,8 +323,9 @@ const FLOW_PARTS = PART_SPECS.filter((p) => !p.positioned).map((p) => p.sel).joi
|
|
|
316
323
|
const STROKE_PARTS = PART_SPECS.filter((p) => !p.strokeless).map((p) => p.sel).join(', ');
|
|
317
324
|
const UNCLIPPED = PART_SPECS.filter((p) => !p.clips).map((p) => p.sel).join(', ');
|
|
318
325
|
const CLIPPED = PART_SPECS.filter((p) => p.clips).map((p) => p.sel).join(', ');
|
|
326
|
+
const UNMASKED = PART_SPECS.filter((p) => p.unmasked).map((p) => p.sel).join(', ');
|
|
319
327
|
|
|
320
|
-
export function buildDefsMarkup(s:
|
|
328
|
+
export function buildDefsMarkup(s: SketchStyle): string {
|
|
321
329
|
/**
|
|
322
330
|
* `warp` is the shape stage: one wave of noise whose wavelength spans a whole
|
|
323
331
|
* component, so the four corners sample different parts of the field and the
|
|
@@ -576,7 +584,7 @@ const swing = (travel: number) => String(Number((travel * 2).toFixed(4)));
|
|
|
576
584
|
/** Squares the displacement wave off around 0.5, its zero, so full amplitude is
|
|
577
585
|
spent along the whole edge rather than only where the wave peaks. Both
|
|
578
586
|
channels take it: the map reads x from R and y from G. */
|
|
579
|
-
function squareOff(s:
|
|
587
|
+
function squareOff(s: SketchStyle, from: string, to: string): string {
|
|
580
588
|
if (s.waveform <= 1) return '';
|
|
581
589
|
const slope = s.waveform.toFixed(2);
|
|
582
590
|
const intercept = ((1 - s.waveform) / 2).toFixed(3);
|
|
@@ -586,7 +594,7 @@ function squareOff(s: SketchSettings, from: string, to: string): string {
|
|
|
586
594
|
`</feComponentTransfer>`;
|
|
587
595
|
}
|
|
588
596
|
|
|
589
|
-
const squaredResult = (s:
|
|
597
|
+
const squaredResult = (s: SketchStyle, from: string, to: string) =>
|
|
590
598
|
(s.waveform > 1 ? to : from);
|
|
591
599
|
|
|
592
600
|
/** Along-stroke pressure wavelength. Low, with a high floor in the transfer
|
|
@@ -642,7 +650,7 @@ const RETRACE_SEED = 53;
|
|
|
642
650
|
const WARP_FREQUENCY = 0.08;
|
|
643
651
|
|
|
644
652
|
|
|
645
|
-
export function buildStylesheet(s:
|
|
653
|
+
export function buildStylesheet(s: SketchStyle): string {
|
|
646
654
|
const on = '[data-sketch]';
|
|
647
655
|
const parts = `:is(${PARTS})`;
|
|
648
656
|
const el = `${on} ${parts}`;
|
|
@@ -853,6 +861,10 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
853
861
|
`pointer-events:none;` +
|
|
854
862
|
`}`;
|
|
855
863
|
|
|
864
|
+
/* The parts that float over page content take the fill whole, and everything
|
|
865
|
+
else the layer does with it. The second `:is` outweighs the rule above. */
|
|
866
|
+
const solidFills = s.maskOn ? `${el}:is(${UNMASKED})::before{mask-image:none;}` : '';
|
|
867
|
+
|
|
856
868
|
// The hatch is laid over the fill as a second background LAYER rather than
|
|
857
869
|
// as `background-image` beside a `background-color`, because a part is free
|
|
858
870
|
// to name a gradient as its fill: the Kit's lead block hands the layer the
|
|
@@ -1037,7 +1049,7 @@ export function buildStylesheet(s: SketchSettings): string {
|
|
|
1037
1049
|
}).join('');
|
|
1038
1050
|
|
|
1039
1051
|
return [
|
|
1040
|
-
registrations, vars, icons, host, fill, fillStyles, retrace, stroke,
|
|
1052
|
+
registrations, vars, icons, host, fill, solidFills, fillStyles, retrace, stroke,
|
|
1041
1053
|
seedRotation, shape, jitter, perPart, colours, states,
|
|
1042
1054
|
].join('\n');
|
|
1043
1055
|
}
|
|
@@ -1071,12 +1083,21 @@ function defsNode(doc: Document): SVGSVGElement {
|
|
|
1071
1083
|
* This only makes the effect *available*. An element opts in by carrying
|
|
1072
1084
|
* data-sketch, which is what `setSketchScope` writes.
|
|
1073
1085
|
*/
|
|
1074
|
-
export function applySketchLayer(settings:
|
|
1086
|
+
export function applySketchLayer(settings: SketchStyle): void {
|
|
1075
1087
|
const defs = buildDefsMarkup(settings);
|
|
1076
1088
|
const css = buildStylesheet(settings);
|
|
1077
1089
|
for (const doc of getSyncedDocuments()) {
|
|
1090
|
+
const style = styleNode(doc);
|
|
1091
|
+
// Writing markup a document already has is a visible flash, not a no-op:
|
|
1092
|
+
// rewriting defs destroys every filter the page is mid-paint against, and
|
|
1093
|
+
// rewriting the sheet drops the mask image to be decoded again. Both are
|
|
1094
|
+
// built from the same settings, so the sheet answers for the pair. The
|
|
1095
|
+
// comparison is against the DOM rather than a variable because with the
|
|
1096
|
+
// overlay open two instances of this module render into this page, and the
|
|
1097
|
+
// document is the only ground they share.
|
|
1098
|
+
if (style.textContent === css) continue;
|
|
1078
1099
|
defsNode(doc).innerHTML = defs;
|
|
1079
|
-
|
|
1100
|
+
style.textContent = css;
|
|
1080
1101
|
}
|
|
1081
1102
|
}
|
|
1082
1103
|
|
|
@@ -1094,7 +1115,7 @@ export function removeSketchLayer(): void {
|
|
|
1094
1115
|
* The host page's root and the editor's own preview container are both scopes,
|
|
1095
1116
|
* which is why this takes an element rather than assuming documentElement.
|
|
1096
1117
|
*/
|
|
1097
|
-
export function setSketchScope(el: HTMLElement | null, settings:
|
|
1118
|
+
export function setSketchScope(el: HTMLElement | null, settings: SketchStyle | null): void {
|
|
1098
1119
|
if (!el) return;
|
|
1099
1120
|
if (!settings) {
|
|
1100
1121
|
el.removeAttribute('data-sketch');
|
|
@@ -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;
|
|
103
150
|
}
|
|
104
151
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
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);
|
|
183
|
+
}
|
|
108
184
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
);
|
|
197
|
+
|
|
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 });
|
|
206
|
+
}
|
|
207
|
+
|
|
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());
|
|
111
215
|
}
|
|
112
216
|
|
|
113
|
-
export async function
|
|
114
|
-
const file = await
|
|
115
|
-
|
|
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,36 +264,152 @@ 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
|
|
|
162
|
-
|
|
163
|
-
|
|
271
|
+
/** The root the effect paints in this document, or null while this document is
|
|
272
|
+
showing an editor surface. Registered by LiveTokensRouter: a consumer can
|
|
273
|
+
relocate the editor routes, so the router is the only thing that knows
|
|
274
|
+
whether what is on screen is a page or the editor's own chrome. Null until
|
|
275
|
+
it says so, which is what keeps the editor from flashing the effect over
|
|
276
|
+
itself between import and first render. */
|
|
277
|
+
let pageRoot: HTMLElement | null = null;
|
|
164
278
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
279
|
+
let installed = false;
|
|
280
|
+
|
|
281
|
+
function render(enabled: boolean, settings: SketchStyle): void {
|
|
282
|
+
if (typeof document === 'undefined') return;
|
|
283
|
+
if (!enabled) {
|
|
284
|
+
if (installed) removeSketchLayer();
|
|
285
|
+
installed = false;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
applySketchLayer(settings);
|
|
289
|
+
installed = true;
|
|
290
|
+
// The editor's own chrome must never pick the effect up. Two roots qualify:
|
|
291
|
+
// the host page behind the overlay iframe, and this document while it is
|
|
292
|
+
// showing a page. The preview container scopes itself.
|
|
293
|
+
setSketchScope(hostRoot(), settings);
|
|
294
|
+
setSketchScope(pageRoot, settings);
|
|
295
|
+
}
|
|
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);
|
|
172
308
|
installed = true;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
|
|
341
|
+
export function setSketchPageRoot(el: HTMLElement | null): void {
|
|
342
|
+
if (el === pageRoot) return;
|
|
343
|
+
setSketchScope(pageRoot, null);
|
|
344
|
+
pageRoot = el;
|
|
345
|
+
render(get(sketchEnabled), get(sketchSettings));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* What this document last exchanged with the other one, per key: written by
|
|
350
|
+
* `share`, recorded by `adopt`.
|
|
351
|
+
*
|
|
352
|
+
* A document must never write back a value it adopted. Comparing against its
|
|
353
|
+
* own store is not enough, because the echo is not identical, it is LATE: a
|
|
354
|
+
* drag sends a value every frame, the far side adopts the first and writes it
|
|
355
|
+
* back, and by then this side is three frames along. It reads the echo, sees a
|
|
356
|
+
* value it does not hold, and adopts its own past — the handle jumps back under
|
|
357
|
+
* the cursor and the drag cannot move.
|
|
358
|
+
*/
|
|
359
|
+
const shared = new Map<string, string>();
|
|
360
|
+
|
|
361
|
+
function share(key: string, value: string): void {
|
|
362
|
+
if (shared.get(key) === value) return;
|
|
363
|
+
shared.set(key, value);
|
|
364
|
+
persist(key, value);
|
|
365
|
+
}
|
|
177
366
|
|
|
367
|
+
function adopt(key: string, value: string): void {
|
|
368
|
+
shared.set(key, value);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof document !== 'undefined') {
|
|
178
372
|
sketchEnabled.subscribe((enabled) => {
|
|
179
|
-
|
|
373
|
+
share(ENABLED_KEY, String(enabled));
|
|
180
374
|
render(enabled, get(sketchSettings));
|
|
181
375
|
});
|
|
182
376
|
|
|
183
377
|
sketchSettings.subscribe((settings) => {
|
|
184
|
-
|
|
378
|
+
share(SETTINGS_KEY, JSON.stringify(settings));
|
|
185
379
|
render(get(sketchEnabled), settings);
|
|
186
380
|
});
|
|
187
381
|
|
|
188
|
-
|
|
189
|
-
sketchBaseline.subscribe((b) =>
|
|
382
|
+
sketchStyleName.subscribe((name) => share(STYLE_NAME_KEY, name));
|
|
383
|
+
sketchBaseline.subscribe((b) => share(BASELINE_KEY, b ? JSON.stringify(b) : ''));
|
|
384
|
+
|
|
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. */
|
|
390
|
+
window.addEventListener('storage', (event) => {
|
|
391
|
+
if (event.storageArea !== localStorage) return;
|
|
392
|
+
if (event.key === ENABLED_KEY) {
|
|
393
|
+
const next = event.newValue === 'true';
|
|
394
|
+
adopt(ENABLED_KEY, String(next));
|
|
395
|
+
if (next !== get(sketchEnabled)) sketchEnabled.set(next);
|
|
396
|
+
} else if (event.key === SETTINGS_KEY) {
|
|
397
|
+
if (event.newValue && event.newValue !== JSON.stringify(get(sketchSettings))) {
|
|
398
|
+
const next = readSettings();
|
|
399
|
+
adopt(SETTINGS_KEY, JSON.stringify(next));
|
|
400
|
+
sketchSettings.set(next);
|
|
401
|
+
}
|
|
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);
|
|
406
|
+
} else if (event.key === BASELINE_KEY) {
|
|
407
|
+
const current = get(sketchBaseline);
|
|
408
|
+
if ((event.newValue || '') !== (current ? JSON.stringify(current) : '')) {
|
|
409
|
+
const next = readBaseline();
|
|
410
|
+
adopt(BASELINE_KEY, next ? JSON.stringify(next) : '');
|
|
411
|
+
sketchBaseline.set(next);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
190
415
|
}
|