@motion-proto/live-tokens 0.85.0 → 0.87.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/editor/component-editor/SlidePagerEditor.svelte +118 -0
  5. package/src/editor/component-editor/VideoFrameEditor.svelte +73 -0
  6. package/src/editor/component-editor/VideoLightboxEditor.svelte +108 -0
  7. package/src/editor/component-editor/registry.ts +40 -1
  8. package/src/editor/core/sketch/sketchLayer.ts +7 -0
  9. package/src/editor/overlay/LiveEditorOverlay.svelte +45 -52
  10. package/src/editor/pages/ComponentEditorPage.svelte +1 -1
  11. package/src/editor/ui/ThemePanel.svelte +9 -53
  12. package/src/live-tokens/data/themes/autumn.json +97 -0
  13. package/src/live-tokens/data/themes/halloween.json +97 -0
  14. package/src/live-tokens/data/themes/midnight-study.json +97 -0
  15. package/src/live-tokens/data/themes/ocean.json +97 -0
  16. package/src/live-tokens/data/themes/royal-velvet.json +97 -0
  17. package/src/live-tokens/data/themes/sketchy.json +97 -0
  18. package/src/live-tokens/data/themes/spring-meadow.json +97 -0
  19. package/src/live-tokens/data/themes/sunset.json +97 -0
  20. package/src/system/assets/slide-04.webp +0 -0
  21. package/src/system/assets/slide-05.webp +0 -0
  22. package/src/system/assets/slide-06.webp +0 -0
  23. package/src/system/assets/watch-navigation-poster.webp +0 -0
  24. package/src/system/assets/watch-navigation.mp4 +0 -0
  25. package/src/system/components/ImageLightbox.svelte +20 -21
  26. package/src/system/components/SlidePager.svelte +516 -0
  27. package/src/system/components/VideoFrame.svelte +284 -0
  28. package/src/system/components/VideoLightbox.svelte +454 -0
  29. package/src/testing-js/{chunk-RDHCBQ6I.js → chunk-FTLASW2R.js} +366 -2
  30. package/src/testing-js/chunk-FTLASW2R.js.map +1 -0
  31. package/src/testing-js/{chunk-XV5CYADO.js → chunk-NXRFTTVL.js} +10 -6
  32. package/src/testing-js/chunk-NXRFTTVL.js.map +1 -0
  33. package/src/testing-js/component-behavior.contract.js +1 -1
  34. package/src/testing-js/component-editor.contract.js +1 -1
  35. package/src/testing-js/component-render.contract.js +1 -1
  36. package/src/testing-js/index.d.ts +2 -2
  37. package/src/testing-js/index.js +2 -2
  38. package/src/testing-js/page-compliance.contract.js +1 -1
  39. package/src/testing-js/{vitest-BMLIbDs2.d.ts → vitest-5vWtqwWe.d.ts} +2 -2
  40. package/src/testing-js/vitest.d.ts +1 -1
  41. package/src/testing-js/vitest.js +2 -2
  42. package/src/testing-js/chunk-RDHCBQ6I.js.map +0 -1
  43. package/src/testing-js/chunk-XV5CYADO.js.map +0 -1
@@ -0,0 +1,454 @@
1
+ <script module lang="ts">
2
+ import type { CatalogueEntry } from '../../editor/component-editor/scaffolding/types';
3
+
4
+ export const catalogue = {
5
+ description:
6
+ 'A small still that fades the page away and plays its clip in the middle of the screen when clicked.',
7
+ whenToUse:
8
+ 'a short clip on a plain ground, such as a product shot, that the reader opens from a thumbnail beside the copy.',
9
+ whenNotToUse: [
10
+ { when: 'a picture whose detail is the point.', use: 'imagelightbox' },
11
+ { when: 'footage that plays in the layout at full size, with no thumbnail to open.', use: 'videoframe' }
12
+ ],
13
+ props: {
14
+ chrome:
15
+ '`badge` lays a play badge over the thumbnail, so a still reads as a clip. `none` shows the still alone.',
16
+ inline: 'true renders the open lightbox in flow, without the thumbnail. The editor preview uses it.',
17
+ loop: 'true repeats the clip.',
18
+ hoverPlay:
19
+ 'true plays the thumbnail muted under the pointer, and rewinds it on the way out. A reader who asked for less motion keeps the still.',
20
+ stretch:
21
+ 'true spreads the trigger over the nearest positioned ancestor, so a whole card opens the clip. The consumer positions that ancestor.',
22
+ thumbAspect:
23
+ 'the ratio the thumbnail box holds, when it should not be the clip\'s own. The clip still opens at its full frame.',
24
+ thumbFit:
25
+ '`cover` fills a thumbnail box the clip does not share the shape of, and crops what will not fit. `contain` keeps the whole frame and letterboxes it.',
26
+ previewStart:
27
+ 'seconds into the clip the hover preview starts, for footage that opens on a title card. The clip itself still opens at the beginning.'
28
+ }
29
+ } satisfies CatalogueEntry;
30
+ </script>
31
+
32
+ <script lang="ts">
33
+ import { tick } from 'svelte';
34
+ import { portal } from '../internal/portal';
35
+
36
+ interface Props {
37
+ src: string;
38
+ /** The thumbnail, and the frame shown before the clip starts. */
39
+ poster: string;
40
+ alt?: string;
41
+ /** Natural size of the clip. Sets the thumbnail's box and the open size. */
42
+ width?: number;
43
+ height?: number;
44
+ loop?: boolean;
45
+ /** "badge" lays a play badge over the thumbnail. */
46
+ chrome?: 'badge' | 'none';
47
+ /** Renders the open lightbox in flow, without the thumbnail. Used by the editor preview. */
48
+ inline?: boolean;
49
+ /** Plays the thumbnail muted under the pointer. Still for reduced motion. */
50
+ hoverPlay?: boolean;
51
+ /** Spreads the hit area over the nearest positioned ancestor. */
52
+ stretch?: boolean;
53
+ /** The thumbnail box's ratio, when it differs from the clip's own. */
54
+ thumbAspect?: number;
55
+ /** How the still meets a box it does not share the shape of. */
56
+ thumbFit?: 'contain' | 'cover';
57
+ /** Seconds in that the hover preview starts. The open clip still plays from 0. */
58
+ previewStart?: number;
59
+ /** Editor preview hook: `force-hover` paints the hover of the close button on an `inline` lightbox, and of the badge on a tile. */
60
+ class?: string;
61
+ }
62
+
63
+ let {
64
+ src,
65
+ poster,
66
+ alt = '',
67
+ width = undefined,
68
+ height = undefined,
69
+ loop = false,
70
+ chrome = 'none',
71
+ inline = false,
72
+ hoverPlay = false,
73
+ stretch = false,
74
+ thumbAspect = undefined,
75
+ thumbFit = 'contain',
76
+ previewStart = 0,
77
+ class: className = ''
78
+ }: Props = $props();
79
+
80
+ let thumbEl: HTMLVideoElement | undefined = $state();
81
+
82
+ function previewIn() {
83
+ if (!thumbEl || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
84
+ if (thumbEl.currentTime < previewStart) thumbEl.currentTime = previewStart;
85
+ thumbEl.play().catch(() => {});
86
+ }
87
+
88
+ function previewOut() {
89
+ if (!thumbEl) return;
90
+ thumbEl.pause();
91
+ thumbEl.currentTime = previewStart;
92
+ }
93
+
94
+ let triggerEl: HTMLButtonElement | undefined = $state();
95
+ let modalEl: HTMLDivElement | undefined = $state();
96
+ let videoEl: HTMLVideoElement | undefined = $state();
97
+ let closeBtnEl: HTMLButtonElement | undefined = $state();
98
+ // `mounted` keeps the lightbox in the DOM through the fade out, `shown`
99
+ // drives the fade, and `open` is what the trigger reports.
100
+ let mounted = $state(false);
101
+ let shown = $state(false);
102
+ let open = $state(false);
103
+
104
+ const ratio = $derived(width && height ? width / height : undefined);
105
+ // The box the still sits in; the clip itself always opens at `ratio`.
106
+ const tileRatio = $derived(thumbAspect ?? ratio);
107
+
108
+ async function openLightbox() {
109
+ if (open) return;
110
+ open = true;
111
+ mounted = true;
112
+ await tick();
113
+ if (!modalEl) return;
114
+ // Commit the transparent start so the class change below fades in.
115
+ modalEl.getBoundingClientRect();
116
+ shown = true;
117
+ modalEl.focus();
118
+ videoEl?.play();
119
+ }
120
+
121
+ async function closeLightbox() {
122
+ if (!open || !modalEl) return;
123
+ open = false;
124
+ shown = false;
125
+ videoEl?.pause();
126
+ await tick();
127
+ await Promise.all(modalEl.getAnimations().map((a) => a.finished)).catch(() => {});
128
+ if (open) return;
129
+ mounted = false;
130
+ triggerEl?.focus();
131
+ }
132
+
133
+ // A page may act on keys it hears at the window, such as a deck turning
134
+ // slides, so the lightbox keeps every key to itself while it is open.
135
+ function onModalKey(e: KeyboardEvent) {
136
+ e.stopPropagation();
137
+ if (e.key === 'Escape') {
138
+ closeLightbox();
139
+ } else if (e.key === ' ' && e.target === modalEl) {
140
+ e.preventDefault();
141
+ if (videoEl?.paused) videoEl.play();
142
+ else videoEl?.pause();
143
+ } else if (e.key === 'Tab') {
144
+ e.preventDefault();
145
+ (document.activeElement === closeBtnEl ? modalEl : closeBtnEl)?.focus();
146
+ }
147
+ }
148
+ </script>
149
+
150
+ {#if inline}
151
+ <div class="videolightbox-modal inline shown {className}" style:--videolightbox-ratio={ratio}>
152
+ <!-- svelte-ignore a11y_media_has_caption -->
153
+ <video class="videolightbox-video" {src} {poster} preload="metadata" playsinline></video>
154
+ <button
155
+ type="button"
156
+ class="videolightbox-close"
157
+ aria-label="Close" tabindex="-1"
158
+ >
159
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
160
+ <path d="M18 6L6 18" />
161
+ <path d="M6 6l12 12" />
162
+ </svg>
163
+ </button>
164
+ </div>
165
+ {:else}
166
+ <div
167
+ class="videolightbox {className}"
168
+ style:aspect-ratio={tileRatio}
169
+ style:--videolightbox-thumb-fit={thumbFit}
170
+ >
171
+ <button
172
+ bind:this={triggerEl}
173
+ type="button"
174
+ class="videolightbox-trigger"
175
+ class:stretch
176
+ aria-label={alt ? `Play video: ${alt}` : 'Play video'}
177
+ aria-haspopup="dialog"
178
+ aria-expanded={open}
179
+ onclick={openLightbox}
180
+ onpointerenter={hoverPlay ? previewIn : undefined}
181
+ onpointerleave={hoverPlay ? previewOut : undefined}
182
+ onfocus={hoverPlay ? previewIn : undefined}
183
+ onblur={hoverPlay ? previewOut : undefined}
184
+ >
185
+ {#if hoverPlay}
186
+ <!-- Muted and silent by design; the sound belongs to the open clip. -->
187
+ <!-- svelte-ignore a11y_media_has_caption -->
188
+ <video
189
+ bind:this={thumbEl}
190
+ class="videolightbox-thumb"
191
+ {src}
192
+ {poster}
193
+ {width}
194
+ {height}
195
+ muted
196
+ loop
197
+ playsinline
198
+ preload="metadata"
199
+ ></video>
200
+ {:else}
201
+ <img src={poster} alt="" {width} {height} draggable="false" />
202
+ {/if}
203
+ {#if chrome === 'badge'}
204
+ <span class="videolightbox-badge sketch-chip" aria-hidden="true">
205
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.5v13l11-6.5z" /></svg>
206
+ </span>
207
+ {/if}
208
+ </button>
209
+ </div>
210
+
211
+ {#if mounted}
212
+ <!-- Clicking anywhere closes it, the clip included; the close button and
213
+ Escape do the same. -->
214
+ <div
215
+ bind:this={modalEl}
216
+ class="videolightbox-modal"
217
+ class:shown
218
+ role="dialog"
219
+ aria-modal="true"
220
+ aria-label={alt}
221
+ tabindex="-1"
222
+ style:--videolightbox-ratio={ratio}
223
+ style:--videolightbox-natural-width={width ? `${width}px` : undefined}
224
+ use:portal
225
+ onclick={closeLightbox}
226
+ onkeydown={onModalKey}
227
+ >
228
+ <!-- No caption track ships with the source footage. -->
229
+ <!-- svelte-ignore a11y_media_has_caption -->
230
+ <video bind:this={videoEl} class="videolightbox-video" {src} {poster} {loop} playsinline></video>
231
+ <button
232
+ bind:this={closeBtnEl}
233
+ type="button"
234
+ class="videolightbox-close"
235
+ aria-label="Close"
236
+ onclick={closeLightbox}
237
+ >
238
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
239
+ <path d="M18 6L6 18" />
240
+ <path d="M6 6l12 12" />
241
+ </svg>
242
+ </button>
243
+ </div>
244
+ {/if}
245
+ {/if}
246
+
247
+ <style>
248
+ :global(:root) {
249
+ --videolightbox-tile-surface: var(--color-transparent);
250
+ --videolightbox-tile-border: var(--color-transparent);
251
+ --videolightbox-tile-border-width: var(--border-width-0);
252
+ --videolightbox-tile-radius: var(--radius-2xl);
253
+ --videolightbox-tile-shadow: var(--shadow-md);
254
+
255
+ --videolightbox-scrim-surface: var(--scrim-high);
256
+ --videolightbox-overlay-padding: var(--space-64);
257
+ --videolightbox-overlay-duration: var(--duration-500);
258
+ --videolightbox-overlay-easing: var(--ease-out-quad);
259
+
260
+ --videolightbox-chrome-surface: var(--surface-neutral-low);
261
+ --videolightbox-chrome-border: var(--border-brand);
262
+ --videolightbox-chrome-border-width: var(--border-width-1);
263
+ --videolightbox-chrome-radius: var(--radius-full);
264
+ --videolightbox-chrome-icon: var(--text-primary);
265
+ --videolightbox-chrome-hover-surface: var(--surface-brand-high);
266
+
267
+ --videolightbox-badge-default-surface: var(--surface-brand-lower);
268
+ --videolightbox-badge-default-border: var(--border-brand-medium);
269
+ --videolightbox-badge-default-border-width: var(--border-width-2);
270
+ --videolightbox-badge-default-icon: var(--text-primary);
271
+ --videolightbox-badge-default-size: var(--space-64);
272
+ --videolightbox-badge-default-icon-size: var(--icon-size-4xl);
273
+ --videolightbox-badge-duration: var(--duration-200);
274
+ --videolightbox-badge-easing: var(--ease-out-quad);
275
+
276
+ --videolightbox-badge-hover-surface: var(--surface-brand);
277
+ --videolightbox-badge-hover-border: var(--border-brand-strong);
278
+ --videolightbox-badge-hover-icon: var(--text-primary);
279
+ }
280
+
281
+ .videolightbox {
282
+ width: 100%;
283
+ background: var(--videolightbox-tile-surface);
284
+ border: var(--videolightbox-tile-border-width) solid var(--videolightbox-tile-border);
285
+ border-radius: var(--videolightbox-tile-radius);
286
+ box-shadow: var(--videolightbox-tile-shadow);
287
+ }
288
+
289
+ /* The clip lives one level in: the sketch layer forces the drawn part's
290
+ overflow visible, which would let the picture's square corners escape. */
291
+ /* One grid cell stacks the badge on the still without positioning the
292
+ trigger, which would capture a `stretch` pad meant for the ancestor. */
293
+ .videolightbox-trigger {
294
+ display: grid;
295
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
296
+ width: 100%;
297
+ height: 100%;
298
+ padding: 0;
299
+ border: none;
300
+ background: none;
301
+ overflow: hidden;
302
+ border-radius: var(--sketch-radius, var(--videolightbox-tile-radius));
303
+ cursor: pointer;
304
+ }
305
+
306
+ /* The pad is laid over the ancestor the consumer positioned, so the one
307
+ button keeps the label and the whole card answers the pointer. It rides
308
+ above the consumer's own content, or copy beside the still would swallow
309
+ the pointer before the trigger heard it.
310
+ A transform on anything between here and that ancestor would make itself
311
+ the containing block and shrink the pad back to the still. */
312
+ .videolightbox-trigger.stretch::after {
313
+ content: '';
314
+ position: absolute;
315
+ inset: 0;
316
+ z-index: 1;
317
+ }
318
+
319
+ .videolightbox-trigger:focus-visible {
320
+ outline: none;
321
+ box-shadow: var(--ring-focus-md);
322
+ }
323
+
324
+ .videolightbox-trigger img,
325
+ .videolightbox-trigger .videolightbox-thumb {
326
+ grid-area: 1 / 1;
327
+ display: block;
328
+ width: 100%;
329
+ height: 100%;
330
+ object-fit: var(--videolightbox-thumb-fit, contain);
331
+ user-select: none;
332
+ }
333
+
334
+ .videolightbox-badge {
335
+ grid-area: 1 / 1;
336
+ place-self: center;
337
+ box-sizing: border-box;
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ width: var(--videolightbox-badge-default-size);
342
+ height: var(--videolightbox-badge-default-size);
343
+ color: var(--videolightbox-badge-default-icon);
344
+ background: var(--videolightbox-badge-default-surface);
345
+ border: var(--videolightbox-badge-default-border-width) solid
346
+ var(--videolightbox-badge-default-border);
347
+ border-radius: var(--radius-full);
348
+ transition:
349
+ background var(--videolightbox-badge-duration) var(--videolightbox-badge-easing),
350
+ border-color var(--videolightbox-badge-duration) var(--videolightbox-badge-easing),
351
+ color var(--videolightbox-badge-duration) var(--videolightbox-badge-easing);
352
+
353
+ --sketch-fill: var(--videolightbox-badge-default-surface);
354
+ --sketch-stroke: var(--videolightbox-badge-default-border);
355
+ --sketch-hatch-color: var(--videolightbox-badge-default-border);
356
+ --sketch-radius: var(--radius-full);
357
+ --sketch-shadow: none;
358
+ /* Travel is stated in px, and the play triangle is small enough to tear. */
359
+ --sketch-icon-off: var(--sketch-icon-soft);
360
+ }
361
+
362
+ .videolightbox-badge svg {
363
+ width: var(--videolightbox-badge-default-icon-size);
364
+ height: var(--videolightbox-badge-default-icon-size);
365
+ /* The triangle's mass sits left of centre; nudge it back onto the axis. */
366
+ margin-inline-start: 6%;
367
+ }
368
+
369
+ .videolightbox-trigger:hover .videolightbox-badge,
370
+ .videolightbox:global(.force-hover) .videolightbox-badge {
371
+ color: var(--videolightbox-badge-hover-icon);
372
+ background: var(--videolightbox-badge-hover-surface);
373
+ border-color: var(--videolightbox-badge-hover-border);
374
+
375
+ --sketch-fill: var(--videolightbox-badge-hover-surface);
376
+ --sketch-stroke: var(--videolightbox-badge-hover-border);
377
+ --sketch-hatch-color: var(--videolightbox-badge-hover-border);
378
+ }
379
+
380
+ .videolightbox-modal {
381
+ position: fixed;
382
+ inset: 0;
383
+ z-index: var(--z-modal);
384
+ box-sizing: border-box;
385
+ display: grid;
386
+ place-items: center;
387
+ padding: var(--videolightbox-overlay-padding);
388
+ background: var(--videolightbox-scrim-surface);
389
+ container-type: size;
390
+ opacity: 0;
391
+ transition: opacity var(--videolightbox-overlay-duration) var(--videolightbox-overlay-easing);
392
+ cursor: pointer;
393
+ }
394
+
395
+ .videolightbox-modal.shown {
396
+ opacity: 1;
397
+ }
398
+
399
+ .videolightbox-modal:focus {
400
+ outline: none;
401
+ }
402
+
403
+ .videolightbox-modal.inline {
404
+ position: relative;
405
+ inset: auto;
406
+ z-index: auto;
407
+ width: 100%;
408
+ aspect-ratio: 16 / 9;
409
+ cursor: default;
410
+ }
411
+
412
+ .videolightbox-close {
413
+ position: absolute;
414
+ top: var(--space-24);
415
+ right: var(--space-24);
416
+ /* 2.75rem = 44px, the min touch-target floor. */
417
+ width: 2.75rem;
418
+ height: 2.75rem;
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: center;
422
+ padding: 0;
423
+ background: var(--videolightbox-chrome-surface);
424
+ border: var(--videolightbox-chrome-border-width) solid var(--videolightbox-chrome-border);
425
+ border-radius: var(--videolightbox-chrome-radius);
426
+ color: var(--videolightbox-chrome-icon);
427
+ backdrop-filter: blur(var(--blur-md));
428
+ cursor: pointer;
429
+ transition: background var(--duration-200) ease;
430
+ }
431
+
432
+ .videolightbox-close:hover,
433
+ .videolightbox-modal:global(.force-hover) .videolightbox-close {
434
+ background: var(--videolightbox-chrome-hover-surface);
435
+ }
436
+
437
+ .videolightbox-close:focus-visible {
438
+ outline: none;
439
+ box-shadow: var(--ring-focus-md);
440
+ }
441
+
442
+ @media (prefers-reduced-motion: reduce) {
443
+ .videolightbox-modal {
444
+ transition: none;
445
+ }
446
+ }
447
+
448
+ /* The clip plays at its own pixel size and shrinks only to fit the screen. */
449
+ .videolightbox-video {
450
+ display: block;
451
+ width: min(var(--videolightbox-natural-width, 100cqw), 100cqw, 100cqh * var(--videolightbox-ratio, 16 / 9));
452
+ aspect-ratio: var(--videolightbox-ratio, 16 / 9);
453
+ }
454
+ </style>