@motion-proto/live-tokens 0.80.0 → 0.82.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.
@@ -0,0 +1,410 @@
1
+ <script lang="ts">
2
+ import Button from '../../system/components/Button.svelte';
3
+ import SectionDivider from '../../system/components/SectionDivider.svelte';
4
+ import SegmentedControl from '../../system/components/SegmentedControl.svelte';
5
+ import Panel from '../../system/components/Panel.svelte';
6
+ import Section from '../Section.svelte';
7
+
8
+ let { prose = true }: { prose?: boolean } = $props();
9
+
10
+ type Override = 'shape' | 'color';
11
+
12
+ let shape = $state(false);
13
+ let color = $state(false);
14
+ // Tracks the most-recently-applied override so the SegmentedControl can
15
+ // single-highlight it while the underlying applied set stays additive.
16
+ let lastApplied = $state<Override | null>(null);
17
+
18
+ const segments = [
19
+ { value: 'default', label: 'Default', icon: 'fas fa-circle' },
20
+ { value: 'shape', label: 'Shape', icon: 'fas fa-shapes' },
21
+ { value: 'color', label: 'Color', icon: 'fas fa-droplet' },
22
+ ];
23
+
24
+ const variants = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'] as const;
25
+
26
+ const shapeOverride: Record<string, string> = (() => {
27
+ const out: Record<string, string> = {};
28
+ for (const v of variants) {
29
+ out[`--button-${v}-radius`] = 'var(--radius-sm)';
30
+ out[`--button-${v}-border-width`] = 'var(--border-width-2)';
31
+ out[`--button-${v}-text-font-family`] = 'var(--font-serif)';
32
+ out[`--button-${v}-text-font-size`] = 'var(--font-size-2xl)';
33
+ out[`--button-${v}-padding`] = 'var(--space-6)';
34
+ }
35
+ return out;
36
+ })();
37
+
38
+ const colorOverride: Record<string, string> = {
39
+ '--button-primary-surface': 'var(--surface-special-high)',
40
+ '--button-primary-border': 'var(--border-special)',
41
+ '--button-primary-hover-surface': 'var(--surface-special-higher)',
42
+ '--button-primary-hover-border': 'var(--border-special-strong)',
43
+
44
+ '--button-outline-border': 'var(--border-special)',
45
+ '--button-outline-text': 'var(--text-special)',
46
+ '--button-outline-hover-border': 'var(--border-special-strong)',
47
+
48
+ '--button-success-surface': 'var(--surface-accent-low)',
49
+ '--button-success-border': 'var(--border-accent)',
50
+ '--button-success-text': 'var(--text-accent)',
51
+ '--button-success-hover-surface': 'var(--surface-accent-higher)',
52
+ '--button-success-hover-border': 'var(--border-accent-strong)',
53
+
54
+ '--button-warning-surface': 'var(--surface-info-low)',
55
+ '--button-warning-border': 'var(--border-info)',
56
+ '--button-warning-text': 'var(--text-info)',
57
+ '--button-warning-hover-surface': 'var(--surface-info-high)',
58
+ '--button-warning-hover-border': 'var(--border-info-medium)',
59
+
60
+ '--button-danger-surface': 'var(--surface-brand-low)',
61
+ '--button-danger-border': 'var(--border-brand)',
62
+ '--button-danger-text': 'var(--text-brand)',
63
+ '--button-danger-hover-surface': 'var(--surface-brand-high)',
64
+ '--button-danger-hover-border': 'var(--border-brand-medium)',
65
+ };
66
+
67
+ let stageStyle = $derived.by(() => {
68
+ const merged: Record<string, string> = {};
69
+ if (shape) Object.assign(merged, shapeOverride);
70
+ if (color) Object.assign(merged, colorOverride);
71
+ return Object.entries(merged)
72
+ .map(([k, v]) => `${k}: ${v}`)
73
+ .join('; ');
74
+ });
75
+
76
+ // The highlighted segment tracks the most-recently-applied override that
77
+ // is still on. When the last-applied override gets toggled off, the
78
+ // highlight falls back to whatever else is still on, or Default.
79
+ let activeSegment = $derived.by(() => {
80
+ if (lastApplied === 'shape' && shape) return 'shape';
81
+ if (lastApplied === 'color' && color) return 'color';
82
+ if (shape) return 'shape';
83
+ if (color) return 'color';
84
+ return 'default';
85
+ });
86
+
87
+ function handleSelect(v: string) {
88
+ if (v === 'default') {
89
+ shape = false;
90
+ color = false;
91
+ lastApplied = null;
92
+ return;
93
+ }
94
+ if (v === 'shape') {
95
+ shape = !shape;
96
+ lastApplied = shape ? 'shape' : color ? 'color' : null;
97
+ return;
98
+ }
99
+ if (v === 'color') {
100
+ color = !color;
101
+ lastApplied = color ? 'color' : shape ? 'shape' : null;
102
+ }
103
+ }
104
+ </script>
105
+
106
+ <Section
107
+ class="kit-section"
108
+ title="The Kit"
109
+ eyebrow="What's inside"
110
+ description="A basic set of design system components connected to a token library."
111
+ gap="var(--space-8)"
112
+ >
113
+ <div class="kit-grid">
114
+ <div class="kit-item kit-lead sketch-container">
115
+ <div class="kit-head sketch-surface">
116
+ <i class="fas fa-pen-ruler kit-icon"></i>
117
+ <span class="kit-title">Live editor</span>
118
+ </div>
119
+ <span class="kit-rule sketch-rule" aria-hidden="true"></span>
120
+ <div class="kit-body" class:prose>
121
+ <p>
122
+ Edit tokens and components on top of the page and watch every change cascade immediately.
123
+ </p>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="kit-item">
128
+ <div class="kit-head">
129
+ <i class="fas fa-palette kit-icon"></i>
130
+ <span class="kit-title">Design tokens</span>
131
+ </div>
132
+ <span class="kit-rule sketch-rule" aria-hidden="true"></span>
133
+ <div class="kit-body" class:prose><p>A complete set of atomic CSS variables for styling.</p></div>
134
+ </div>
135
+
136
+ <div class="kit-item">
137
+ <div class="kit-head">
138
+ <i class="fas fa-puzzle-piece kit-icon"></i>
139
+ <span class="kit-title">Component library</span>
140
+ </div>
141
+ <span class="kit-rule sketch-rule" aria-hidden="true"></span>
142
+ <div class="kit-body" class:prose><p>A customizable set of components linked to the tokens.</p></div>
143
+ </div>
144
+
145
+ <div class="kit-item">
146
+ <div class="kit-head">
147
+ <i class="fas fa-cube kit-icon"></i>
148
+ <span class="kit-title">Ships as CSS</span>
149
+ </div>
150
+ <span class="kit-rule sketch-rule" aria-hidden="true"></span>
151
+ <div class="kit-body" class:prose><p>Standard output. The editor stays in development.</p></div>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="kit-preview">
156
+ <div class="swap-block">
157
+ <div class="swap-control">
158
+ <SectionDivider title="Swap tokens with a click" variant="sm" />
159
+ <SegmentedControl {segments} value={activeSegment} onchange={handleSelect} />
160
+ </div>
161
+
162
+ <Panel style={stageStyle}>
163
+ <div class="stage">
164
+ <span class="stage-eyebrow">Token demo</span>
165
+ <div class="swap-row">
166
+ <div class="stage-cell"><Button>Primary</Button></div>
167
+ <div class="stage-cell"><Button variant="secondary">Secondary</Button></div>
168
+ <div class="stage-cell"><Button variant="outline">Outline</Button></div>
169
+ <div class="stage-cell"><Button variant="success" icon="fas fa-check">Success</Button></div>
170
+ <div class="stage-cell"><Button variant="warning" icon="fas fa-bolt">Warning</Button></div>
171
+ <div class="stage-cell"><Button variant="danger" icon="fas fa-xmark">Danger</Button></div>
172
+ </div>
173
+ </div>
174
+ </Panel>
175
+ </div>
176
+ </div>
177
+ </Section>
178
+
179
+ <style lang="scss">
180
+ @use '../../system/styles/slot-prose' as *;
181
+
182
+ /* The section spans page columns 2–11, so its tracks are the page grid
183
+ minus one column on each side; each item then lands on exactly three page
184
+ columns and the row stops at column 9, leaving column 10 open on the right. */
185
+ .kit-grid {
186
+ display: grid;
187
+ grid-template-columns: repeat(calc(var(--columns-count) - 2), 1fr);
188
+ column-gap: var(--columns-gutter);
189
+ row-gap: var(--space-32);
190
+ }
191
+
192
+ .kit-item {
193
+ grid-column: span 3;
194
+ display: flex;
195
+ flex-direction: column;
196
+ }
197
+
198
+ .kit-head {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: var(--space-12);
202
+ padding: 0 0 var(--space-12);
203
+ }
204
+
205
+ /* Title, body and icon mirror the Card typography used in the Layers section. */
206
+ .kit-icon {
207
+ font-size: var(--icon-size-2xl);
208
+ color: var(--text-primary);
209
+ }
210
+
211
+ .kit-title {
212
+ color: var(--text-primary);
213
+ font-family: var(--font-sans);
214
+ font-size: var(--font-size-2xl);
215
+ font-weight: var(--font-weight-medium);
216
+ line-height: var(--line-height-normal);
217
+ }
218
+
219
+ .kit-body {
220
+ padding: var(--space-12) 0 0;
221
+ font-family: var(--font-sans);
222
+ font-size: var(--font-size-xl);
223
+ line-height: var(--line-height-normal);
224
+ font-weight: var(--font-weight-normal);
225
+ color: var(--text-secondary);
226
+
227
+ @include slot-prose;
228
+ }
229
+
230
+ /* The pink hairline is the section's whole structural device: a horizontal
231
+ rule under each item's title, and a vertical rule splitting the lead.
232
+ It is an element rather than a border on .kit-head so that Sketch mode can
233
+ redraw it — the effect displaces boxes, and a border is not one. */
234
+ .kit-rule {
235
+ height: var(--border-width-2);
236
+ background: var(--border-brand);
237
+ --sketch-fill: var(--border-brand);
238
+ }
239
+
240
+ .kit-lead .kit-rule {
241
+ display: none;
242
+ }
243
+
244
+ /* Lead spans the full row; icon + title sit in the first column track, copy
245
+ fills the rest, mirroring the three columns below. */
246
+ /* Subgrid inherits the parent's 10 tracks so the head and copy align to real
247
+ columns: head centered on tracks 1–3 (page cols 2–4), copy on tracks 4–9
248
+ (page cols 5–10), with the last track left open like the items below. */
249
+ .kit-lead {
250
+ grid-column: 1 / -1;
251
+ display: grid;
252
+ grid-template-columns: subgrid;
253
+ align-items: center;
254
+ border-radius: var(--radius-lg);
255
+ /* Light brand wash strongest behind the "Live editor" head, dissolving
256
+ across the full block to nothing at the right edge — head and copy both
257
+ sit inside the fade. Declared once and handed to the sketch layer too:
258
+ the effect blanks the real background, so a wash named only here would
259
+ vanish when Sketch mode is on. */
260
+ --kit-lead-wash: linear-gradient(
261
+ 90deg,
262
+ color-mix(in srgb, var(--color-brand-500) 21%, transparent),
263
+ transparent
264
+ );
265
+ background: var(--kit-lead-wash);
266
+ --sketch-fill: var(--kit-lead-wash);
267
+ --sketch-stroke: transparent;
268
+ /* The block draws no outline, and hatching takes its ink from the outline
269
+ unless a part names its own. */
270
+ --sketch-hatch-color: var(--color-brand-500);
271
+ }
272
+
273
+ /* The head only carries a surface of its own when stacked (see the 960
274
+ breakpoint). Here it has none, and --sketch-fill inherits, so without this
275
+ the layer would draw the lead's wash a second time behind the head. */
276
+ .kit-lead .kit-head {
277
+ grid-column: 1 / span 3;
278
+ justify-content: center;
279
+ gap: var(--space-16);
280
+ padding: var(--space-32) 0;
281
+ --sketch-fill: transparent;
282
+ --sketch-stroke: transparent;
283
+ --sketch-radius: 0px;
284
+ }
285
+
286
+ .kit-lead .kit-icon {
287
+ font-size: var(--icon-size-4xl, 3rem);
288
+ }
289
+
290
+ .kit-lead .kit-title {
291
+ font-size: var(--font-size-4xl);
292
+ font-weight: var(--font-weight-bold);
293
+ }
294
+
295
+ .kit-lead .kit-body {
296
+ grid-column: 4 / span 6;
297
+ padding: var(--space-32) 0;
298
+ font-size: var(--font-size-2xl);
299
+ color: var(--text-primary);
300
+ }
301
+
302
+ /* Controls sit above; the panel below is just the demo surface. */
303
+ /* Same 10-track grid as the cards so the block lands on real columns. */
304
+ .kit-preview {
305
+ margin-top: var(--space-32);
306
+ display: grid;
307
+ grid-template-columns: repeat(calc(var(--columns-count) - 2), 1fr);
308
+ column-gap: var(--columns-gutter);
309
+ }
310
+
311
+ /* Tracks 1–9 (page cols 2–10), leaving the last column open like the items
312
+ above. Label pins to the left column, control to the right. */
313
+ .swap-block {
314
+ grid-column: 1 / 10;
315
+ display: flex;
316
+ flex-direction: column;
317
+ align-items: stretch;
318
+ gap: var(--space-12);
319
+ }
320
+
321
+ .swap-control {
322
+ margin-top: var(--space-32);
323
+ display: flex;
324
+ flex-direction: row;
325
+ flex-wrap: wrap;
326
+ align-items: center;
327
+ justify-content: space-between;
328
+ gap: var(--space-24);
329
+ }
330
+
331
+ /* Use the sm divider purely as a styled label beside the control: drop its
332
+ built-in margin, and hide its hairline (gated by --_divider-hairline, which
333
+ no longer matches any @container query when set to none). */
334
+ .swap-control :global(.section-divider) {
335
+ margin: 0;
336
+ --_divider-hairline: none;
337
+ }
338
+
339
+ /* Two cells of one width, shrink-wrapped to the left. `width: max-content`
340
+ leaves the fr tracks an indefinite parent to resolve against, so both take
341
+ the wider select's size instead of splitting the block. */
342
+ .stage {
343
+ display: flex;
344
+ flex-direction: column;
345
+ align-items: flex-start;
346
+ gap: var(--space-12);
347
+ }
348
+
349
+ .stage-eyebrow {
350
+ align-self: center;
351
+ font-family: var(--font-sans);
352
+ font-size: var(--font-size-md);
353
+ font-weight: var(--font-weight-medium);
354
+ letter-spacing: var(--letter-spacing-wide);
355
+ text-transform: uppercase;
356
+ color: var(--text-secondary);
357
+ }
358
+
359
+ .swap-row {
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ gap: var(--space-12);
363
+ justify-content: center;
364
+ align-items: center;
365
+ /* Reserved so toggling Shape (taller buttons) doesn't reflow the panel. */
366
+ min-height: 4rem;
367
+ }
368
+
369
+ .stage-cell {
370
+ width: 8.5rem;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ }
375
+
376
+ @media (max-width: 960px) {
377
+ .kit-grid {
378
+ grid-template-columns: 1fr;
379
+ }
380
+ .kit-item {
381
+ grid-column: auto;
382
+ }
383
+ .swap-block {
384
+ grid-column: 1 / -1;
385
+ }
386
+
387
+ .kit-lead {
388
+ display: flex;
389
+ flex-direction: column;
390
+ align-items: stretch;
391
+ background: none;
392
+ --sketch-fill: none;
393
+ }
394
+ /* Stacked: the head gets its own tinted bar instead of the fade. It draws
395
+ no outline, so the hatching takes the lead block's ink. */
396
+ .kit-lead .kit-head {
397
+ justify-content: flex-start;
398
+ gap: var(--space-12);
399
+ padding: var(--space-16) var(--space-20);
400
+ --kit-head-tint: color-mix(in srgb, var(--color-brand-500) 12%, transparent);
401
+ background: var(--kit-head-tint);
402
+ --sketch-fill: var(--kit-head-tint);
403
+ --sketch-hatch-color: var(--color-brand-500);
404
+ }
405
+ .kit-lead .kit-body {
406
+ padding: var(--space-16) var(--space-20);
407
+ font-size: var(--font-size-xl);
408
+ }
409
+ }
410
+ </style>
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import Card from '../../system/components/Card.svelte';
3
+ import Section from '../Section.svelte';
4
+ </script>
5
+
6
+ <Section
7
+ class="layers"
8
+ title="Two composable layers."
9
+ eyebrow="Under the hood"
10
+ description="The same two layers you just saw move — now with the wiring that connects them."
11
+ gap="var(--space-8)"
12
+ >
13
+ <div class="arch">
14
+ <Card class="arch-card arch-base" hover={false} icon="fas fa-layer-group" title="Tokens make the foundation">
15
+ <p>
16
+ Raw CSS variables for every primitive, grouped by family and function. Edit one value and every consumer updates at once.
17
+ </p>
18
+ </Card>
19
+
20
+ <Card class="arch-card arch-component" hover={false} icon="fas fa-shapes" title="Components build upon it">
21
+ <p>
22
+ Each component property is assigned a token. Property names describe purpose. Assign a new token and the component changes.
23
+ </p>
24
+ </Card>
25
+ </div>
26
+ </Section>
27
+
28
+ <style>
29
+ .arch {
30
+ display: grid;
31
+ grid-template-columns: repeat(2, 1fr);
32
+ column-gap: var(--columns-gutter);
33
+ row-gap: var(--space-32);
34
+ }
35
+
36
+ .arch :global(.arch-card) {
37
+ height: 100%;
38
+ }
39
+
40
+ .arch :global(code) {
41
+ font-family: var(--font-mono);
42
+ font-size: 0.9em;
43
+ color: var(--text-primary);
44
+ background: var(--tint-low);
45
+ padding: var(--space-2) var(--space-6);
46
+ border-radius: var(--radius-sm);
47
+ }
48
+
49
+ @media (max-width: 960px) {
50
+ .arch {
51
+ grid-template-columns: 1fr;
52
+ }
53
+ }
54
+ </style>
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import offeringUrl from '../../system/assets/offering.webp';
3
+ </script>
4
+
5
+ <section class="offering">
6
+ <img class="offering-image" src={offeringUrl} alt="" />
7
+
8
+ <figure class="offering-quote">
9
+ <blockquote>
10
+ <p>
11
+ <span class="open-quote" aria-hidden="true">&ldquo;</span>I built live
12
+ tokens to help me quickly set up and style single-page apps and microsites.
13
+ I found it useful. I hope you do too.<span class="close-quote" aria-hidden="true">&rdquo;</span>
14
+ </p>
15
+ </blockquote>
16
+ <figcaption>-Mark</figcaption>
17
+ </figure>
18
+ </section>
19
+
20
+ <style>
21
+ .offering {
22
+ grid-column: 1 / -1;
23
+ display: grid;
24
+ grid-template-columns: repeat(var(--columns-count), 1fr);
25
+ column-gap: var(--columns-gutter);
26
+ position: relative;
27
+ z-index: 1;
28
+ }
29
+
30
+ /* Roughly a third of the content width, parked on the left edge so it
31
+ overlaps the left-hand card above without dominating the section. */
32
+ .offering-image {
33
+ grid-column: 2 / 7;
34
+ grid-row: 1;
35
+ display: block;
36
+ width: 100%;
37
+ height: auto;
38
+ border-radius: var(--radius-lg);
39
+ z-index: 1;
40
+ }
41
+
42
+ /* Sits in the open space to the right of the image. No glass background.
43
+ align-self centers it on the image, then translateY drops it by half the
44
+ image's upward overlap (the extra -space-64 in Demo.svelte) so it lands in
45
+ the middle of the actual gap between the cards above and Get Started below. */
46
+ .offering-quote {
47
+ grid-column: 7 / -2;
48
+ grid-row: 1;
49
+ align-self: center;
50
+ transform: translateY(calc(var(--space-64) / 2));
51
+ margin: 0;
52
+ /* Align body text to the card text above. The cards split this same span
53
+ in half, so the right card's left edge sits half a gutter right of line 7;
54
+ its body text is then inset by the card's --space-20 padding. Match both,
55
+ and the open quote dangles left into this padding (text-indent on p). */
56
+ padding-left: calc(var(--columns-gutter) / 2 + var(--space-20));
57
+ }
58
+
59
+ .offering-quote blockquote {
60
+ margin: 0;
61
+ padding: 0;
62
+ border: none;
63
+ background: none;
64
+ border-radius: 0;
65
+ color: inherit;
66
+ font-style: normal;
67
+ }
68
+
69
+ .offering-quote p {
70
+ font-family: var(--font-serif);
71
+ font-size: var(--font-size-3xl);
72
+ font-weight: var(--font-weight-normal);
73
+ color: var(--text-primary);
74
+ line-height: 1.2;
75
+ margin: 0;
76
+ hyphens: none;
77
+ text-indent: -0.55em;
78
+ }
79
+
80
+ .open-quote,
81
+ .close-quote {
82
+ font-family: var(--font-serif);
83
+ color: var(--color-brand-300);
84
+ }
85
+
86
+ .open-quote {
87
+ margin-right: 0.2em;
88
+ }
89
+
90
+ .close-quote {
91
+ margin-left: 0.05em;
92
+ }
93
+
94
+ .offering-quote figcaption {
95
+ margin-top: 0;
96
+ font-family: var(--font-serif);
97
+ font-weight: var(--font-weight-normal);
98
+ font-size: var(--font-size-3xl);
99
+ font-variation-settings: 'opsz' 144, 'SOFT' 100;
100
+ letter-spacing: 0.01em;
101
+ color: var(--text-primary);
102
+ text-align: right;
103
+ padding-right: 0.4em;
104
+ }
105
+
106
+ @media (max-width: 960px) {
107
+ .offering-image {
108
+ grid-column: 2 / 7;
109
+ }
110
+ .offering-quote {
111
+ grid-column: 7 / -2;
112
+ }
113
+ .offering-quote p {
114
+ font-size: var(--font-size-xl);
115
+ }
116
+ }
117
+
118
+ @media (max-width: 600px) {
119
+ .offering-image {
120
+ grid-column: 1 / -1;
121
+ grid-row: 1;
122
+ }
123
+ .offering-quote {
124
+ grid-column: 1 / -1;
125
+ grid-row: 2;
126
+ align-self: auto;
127
+ width: auto;
128
+ margin: var(--space-16) 0 0;
129
+ padding-left: 0;
130
+ }
131
+ .offering-quote p {
132
+ font-size: var(--font-size-4xl);
133
+ }
134
+ }
135
+ </style>
@@ -0,0 +1,91 @@
1
+ // The theme and the sketchstyle are two separate picks in the demo, but the
2
+ // package holds the sketch layer inside the theme: applying one runs
3
+ // `openThemeSketchSettings`, which overwrites the live dials with whatever the new
4
+ // theme carries, or turns the effect off for a theme carrying none. A visitor
5
+ // who picked Pencil and then tried three themes lost Pencil three times.
6
+ //
7
+ // So the theme select records the look it is leaving, and each document
8
+ // restores it once the apply lands. Each, not just this one: the editor overlay
9
+ // is a second instance of the sketch store in an iframe, it hydrates the
10
+ // applied theme on its own (over BroadcastChannel, so after `applyTheme`
11
+ // resolves here), and it paints the host page as well as its own preview.
12
+ // Restoring only in this document would be overwritten by that copy a tick
13
+ // later — the record in localStorage is what both of them read.
14
+ import { get } from 'svelte/store';
15
+ import { hasPersistedSketchState } from '../editor/core/sketch';
16
+ import {
17
+ sketchBaseline,
18
+ sketchEnabled,
19
+ sketchSettings,
20
+ selectedSketchStyleId,
21
+ } from '../editor/core/sketch/sketchStore';
22
+ import { THEME_SKETCH_ID, type SketchStyleSettings } from '../editor/core/sketch/sketchStyles';
23
+ import { THEME_APPLIED_EVENT } from '../editor/core/themes/themeDocumentSync';
24
+
25
+ const CARRY_KEY = 'lt-demo.sketchCarry';
26
+
27
+ // Long enough for the broadcast to reach the editor frame, short enough that a
28
+ // theme opened later from the editor's own Theme panel — where the sketchstyle
29
+ // IS the theme's, and loading one means taking it — never reads a record left
30
+ // over from the demo.
31
+ const CARRY_WINDOW_MS = 4000;
32
+
33
+ interface Carry {
34
+ token: string;
35
+ at: number;
36
+ enabled: boolean;
37
+ name: string;
38
+ settings: SketchStyleSettings;
39
+ baseline: SketchStyleSettings | null;
40
+ }
41
+
42
+ /** Hold the live sketchstyle across the theme apply that follows. */
43
+ export function carrySketch(): void {
44
+ // A browser that has decided nothing has no pick to keep, and letting the
45
+ // theme seed it is the only way a theme's own sketchstyle ever shows.
46
+ if (!hasPersistedSketchState()) return;
47
+ // "Theme" is a pick that means follow the theme, so it follows this one too.
48
+ if (get(selectedSketchStyleId) === THEME_SKETCH_ID) return;
49
+ const record: Carry = {
50
+ token: Math.random().toString(36).slice(2),
51
+ at: Date.now(),
52
+ enabled: get(sketchEnabled),
53
+ name: get(selectedSketchStyleId),
54
+ settings: get(sketchSettings),
55
+ baseline: get(sketchBaseline),
56
+ };
57
+ try {
58
+ localStorage.setItem(CARRY_KEY, JSON.stringify(record));
59
+ } catch {
60
+ // Storage blocked (private mode); the theme's sketchstyle wins, as before.
61
+ }
62
+ }
63
+
64
+ function readCarry(): Carry | null {
65
+ try {
66
+ const raw = localStorage.getItem(CARRY_KEY);
67
+ if (!raw) return null;
68
+ const record = JSON.parse(raw) as Carry;
69
+ return Date.now() - record.at < CARRY_WINDOW_MS ? record : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function installSketchCarry(): void {
76
+ let handled = '';
77
+ document.addEventListener(THEME_APPLIED_EVENT, () => {
78
+ const record = readCarry();
79
+ if (!record || record.token === handled) return;
80
+ handled = record.token;
81
+ // `themeSketchSettings` stays as the apply left it: what the open theme
82
+ // holds is what makes the restored style read as off the theme, and what
83
+ // fills the picker's Unsaved row.
84
+ selectedSketchStyleId.set(record.name);
85
+ sketchBaseline.set(record.baseline);
86
+ sketchSettings.set(record.settings);
87
+ // Last: every write above repaints, and this is the one that decides
88
+ // whether anything is painted at all.
89
+ sketchEnabled.set(record.enabled);
90
+ });
91
+ }