@motion-proto/live-tokens 0.85.0 → 0.86.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 +44 -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 +95 -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 +86 -0
  13. package/src/live-tokens/data/themes/halloween.json +86 -0
  14. package/src/live-tokens/data/themes/midnight-study.json +86 -0
  15. package/src/live-tokens/data/themes/ocean.json +86 -0
  16. package/src/live-tokens/data/themes/royal-velvet.json +86 -0
  17. package/src/live-tokens/data/themes/sketchy.json +86 -0
  18. package/src/live-tokens/data/themes/spring-meadow.json +86 -0
  19. package/src/live-tokens/data/themes/sunset.json +86 -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 +381 -0
  29. package/src/testing-js/{chunk-RDHCBQ6I.js → chunk-OHU5BHGZ.js} +344 -2
  30. package/src/testing-js/chunk-OHU5BHGZ.js.map +1 -0
  31. package/src/testing-js/{chunk-XV5CYADO.js → chunk-Y6R7Z7VK.js} +10 -6
  32. package/src/testing-js/chunk-Y6R7Z7VK.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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.86.0 — Video and slides ship as components
4
+
5
+ ### Added
6
+
7
+ - **VideoLightbox.** A still that fades the page away and plays its clip in
8
+ the middle of the screen. A click anywhere, the close button, or Escape
9
+ closes it. Tile, scrim, and close-button chrome are editable, with a hover
10
+ state for the close button. Every shipped theme carries it, derived from
11
+ that theme's ImageLightbox so the two match.
12
+
13
+ - **VideoFrame.** It plays a clip in place from a poster frame, with a play
14
+ badge or no chrome at all, and falls back to the poster when the clip is
15
+ not served.
16
+
17
+ - **SlidePager.** It flips a deck of slides in place. Its full-screen view
18
+ pages through every slide with the same bar and the arrow keys, and has a
19
+ close button and Tab stops on its controls. Its chevrons are inline SVG, so
20
+ it needs no icon font.
21
+
22
+ Every shipped theme carries VideoFrame and SlidePager, with frame corners
23
+ and borders taken from that theme's ImageLightbox and Card. The catalogue
24
+ entries of the four media components point to each other.
25
+
26
+ ### Changed
27
+
28
+ - **Every ImageLightbox shows a close button.** A plain lightbox closed only
29
+ on a click, and nothing on screen said so. The close button now renders for
30
+ every lightbox and holds focus when the modal opens. An extended lightbox
31
+ also closes on a click to its unzoomed image; zoomed, a click still pans.
32
+
33
+ - **The editor overlay picks the active page from a dropdown.** The theme
34
+ panel's Components and Sketchstyle rows lose their Open buttons, since the
35
+ view switcher at the top of the panel already opens both. The font pairing
36
+ moves into the Colors & Type info box.
37
+
38
+ ### Fixed
39
+
40
+ - **`check-component --tests` collects only the installed package's
41
+ contracts.** `createVitestConfig` globbed the whole project, so a nested
42
+ project with its own copy of the package in `node_modules` added that
43
+ copy's contracts to the run, and they failed against this project's
44
+ registry. The default now scans the contract files' own directory, the way
45
+ `createPlaywrightConfig` already did.
46
+
3
47
  ## 0.85.0 — A theme's scrims and tints are a colour and three strengths
4
48
 
5
49
  ### Changed
package/README.md CHANGED
@@ -11,7 +11,7 @@ The editor is dev-only. Production builds get plain CSS variables and the compon
11
11
  ## Features
12
12
 
13
13
  - **Live token editing.** Colors, typography, spacing, radii, shadows, motion, palettes, and gradients. Every input writes a CSS variable, so the page repaints with no reload and no build step.
14
- - **Live component editing.** 26 shipped Svelte components (Button, IconButton, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, Panel, InlineEditActions, Slider) declare their design-token aliases in a `:global(:root)` block. Rewire an alias from the component's editor and it updates everywhere that component is used, on your real pages.
14
+ - **Live component editing.** 29 shipped Svelte components (Button, IconButton, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, VideoLightbox, VideoFrame, SlidePager, CodeSnippet, SideNavigation, Panel, InlineEditActions, Slider) declare their design-token aliases in a `:global(:root)` block. Rewire an alias from the component's editor and it updates everywhere that component is used, on your real pages.
15
15
  - **Four dev-only routes.** `/live-tokens/editor` for tokens, `/live-tokens/colors` for palettes, `/live-tokens/components` for per-component aliases, `/live-tokens/docs` for the user guide.
16
16
  - **Editor overlay.** Pins to the top right of every dev page and opens the editor in a side panel or floating window, so you edit on the page you are styling. Its "Page Source" button opens the current page's `.svelte` file in VS Code.
17
17
  - **Themes.** A theme is a whole look in one file: colors and type plus a config for every component, stored by value. Loading one changes a single pointer file, and nothing your site ships changes until you Adopt. Export a theme and import it into another project to restore the look in one step.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -0,0 +1,118 @@
1
+ <script module lang="ts">
2
+ import type { Token } from './scaffolding/types';
3
+
4
+ export const component = 'slidepager';
5
+
6
+ // Single variant: the default carries the frame, the bar, the counter and
7
+ // the control's resting look. Hover and disabled layer on the control alone.
8
+ const states: Record<string, Token[]> = {
9
+ default: [
10
+ { label: 'surface color', element: 'frame', variable: '--slidepager-frame-surface' },
11
+ { label: 'border color', element: 'frame', variable: '--slidepager-frame-border' },
12
+ { label: 'border width', canBeLinked: true, element: 'frame', variable: '--slidepager-frame-border-width' },
13
+ { label: 'corner radius', canBeLinked: true, element: 'frame', variable: '--slidepager-frame-radius' },
14
+ { label: 'shadow', element: 'frame', variable: '--slidepager-frame-shadow' },
15
+ { label: 'gap', canBeLinked: true, element: 'bar', variable: '--slidepager-bar-gap' },
16
+ { label: 'padding', canBeLinked: true, element: 'bar', variable: '--slidepager-bar-padding' },
17
+ { label: 'surface color', element: 'control', variable: '--slidepager-control-surface' },
18
+ { label: 'border color', element: 'control', variable: '--slidepager-control-border' },
19
+ { label: 'border width', canBeLinked: true, element: 'control', variable: '--slidepager-control-border-width' },
20
+ { label: 'corner radius', canBeLinked: true, element: 'control', variable: '--slidepager-control-radius' },
21
+ { label: 'size', canBeLinked: true, element: 'control', variable: '--slidepager-control-size' },
22
+ { label: 'icon color', element: 'control', variable: '--slidepager-control-icon' },
23
+ { label: 'icon size', canBeLinked: true, element: 'control', variable: '--slidepager-control-icon-size' },
24
+ { label: 'text color', element: 'counter', variable: '--slidepager-counter-text' },
25
+ { label: 'font family', element: 'counter', variable: '--slidepager-counter-font-family' },
26
+ { label: 'font size', element: 'counter', variable: '--slidepager-counter-font-size' },
27
+ { label: 'font weight', element: 'counter', variable: '--slidepager-counter-font-weight' },
28
+ { label: 'line height', element: 'counter', variable: '--slidepager-counter-line-height' },
29
+ { label: 'letter spacing', element: 'counter', variable: '--slidepager-counter-letter-spacing' },
30
+ { label: 'scrim color', element: 'overlay', variable: '--slidepager-scrim-surface' },
31
+ { label: 'padding', canBeLinked: true, element: 'overlay', variable: '--slidepager-overlay-padding' },
32
+ { label: 'close button margin', canBeLinked: true, element: 'overlay', variable: '--slidepager-close-margin' },
33
+ { label: 'fade duration', element: 'overlay', variable: '--slidepager-overlay-duration' },
34
+ { label: 'fade easing', element: 'overlay', variable: '--slidepager-overlay-easing' }
35
+ ],
36
+ hover: [
37
+ { label: 'surface color', element: 'control', variable: '--slidepager-control-hover-surface' },
38
+ { label: 'border color', element: 'control', variable: '--slidepager-control-hover-border' },
39
+ { label: 'icon color', element: 'control', variable: '--slidepager-control-hover-icon' }
40
+ ],
41
+ disabled: [
42
+ { label: 'surface color', element: 'control', variable: '--slidepager-control-disabled-surface' },
43
+ { label: 'border color', element: 'control', variable: '--slidepager-control-disabled-border' },
44
+ { label: 'icon color', element: 'control', variable: '--slidepager-control-disabled-icon' }
45
+ ]
46
+ };
47
+
48
+ export const allTokens: Token[] = Object.values(states).flat();
49
+ </script>
50
+
51
+ <script lang="ts">
52
+ import SlidePager, { type SlidePage } from '../../system/components/SlidePager.svelte';
53
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
54
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
55
+ import page04 from '../../system/assets/slide-04.webp';
56
+ import page05 from '../../system/assets/slide-05.webp';
57
+ import page06 from '../../system/assets/slide-06.webp';
58
+
59
+ const demoSlides: SlidePage[] = [
60
+ { src: page04, alt: 'Types of disabilities: visual, auditory, motor, cognitive', width: 960, height: 540 },
61
+ { src: page05, alt: 'Permanent, temporary, or situational', width: 960, height: 540 },
62
+ { src: page06, alt: 'Non-apparent disabilities', width: 960, height: 540 }
63
+ ];
64
+
65
+ // The control's two states are forced on both chevrons at once, so the strip
66
+ // shows each one without a pointer and without landing on an end slide.
67
+ function forceClass(activeState: string) {
68
+ if (activeState === 'hover') return 'force-hover';
69
+ if (activeState === 'disabled') return 'force-disabled';
70
+ return '';
71
+ }
72
+ </script>
73
+
74
+ <ComponentEditorBase
75
+ {component}
76
+ title="Slide Pager"
77
+ description="A deck of slides the reader flips through in place, one at a time. Clicking the frame opens a full-screen read that pages through every slide, with the same bar and a close button. The open preview shows it in place."
78
+ tokens={allTokens}
79
+ >
80
+ <VariantGroup
81
+ name="slidepager"
82
+ title="Slide Pager"
83
+ {states}
84
+ {component}
85
+ elementOrder={['frame', 'bar', 'control', 'counter', 'overlay']}
86
+ >
87
+ {#snippet children({ activeState }: { activeState: string })}
88
+ <div class="slidepager-demo">
89
+ <div class="slidepager-demo-inline">
90
+ <SlidePager slides={demoSlides} index={1} class={forceClass(activeState)} />
91
+ </div>
92
+ <div class="slidepager-demo-open">
93
+ <SlidePager slides={demoSlides} index={1} inline class={forceClass(activeState)} />
94
+ </div>
95
+ </div>
96
+ {/snippet}
97
+ </VariantGroup>
98
+ </ComponentEditorBase>
99
+
100
+ <style>
101
+ .slidepager-demo {
102
+ display: flex;
103
+ flex-wrap: wrap;
104
+ align-items: flex-start;
105
+ gap: var(--ui-space-32);
106
+ width: 100%;
107
+ }
108
+
109
+ .slidepager-demo-inline {
110
+ flex: 1 1 22rem;
111
+ max-width: 32rem;
112
+ }
113
+
114
+ .slidepager-demo-open {
115
+ flex: 1 1 16rem;
116
+ max-width: 22rem;
117
+ }
118
+ </style>
@@ -0,0 +1,73 @@
1
+ <script module lang="ts">
2
+ import type { Token } from './scaffolding/types';
3
+
4
+ export const component = 'videoframe';
5
+
6
+ const states: Record<string, Token[]> = {
7
+ default: [
8
+ { label: 'surface color', element: 'frame', variable: '--videoframe-frame-surface' },
9
+ { label: 'clip surface color', element: 'frame', variable: '--videoframe-clip-surface' },
10
+ { label: 'border color', element: 'frame', variable: '--videoframe-frame-border' },
11
+ { label: 'border width', canBeLinked: true, element: 'frame', variable: '--videoframe-frame-border-width' },
12
+ { label: 'corner radius', canBeLinked: true, element: 'frame', variable: '--videoframe-frame-radius' },
13
+ { label: 'surface color', element: 'badge', variable: '--videoframe-badge-default-surface' },
14
+ { label: 'border color', element: 'badge', variable: '--videoframe-badge-default-border' },
15
+ { label: 'border width', canBeLinked: true, element: 'badge', variable: '--videoframe-badge-default-border-width' },
16
+ { label: 'size', canBeLinked: true, element: 'badge', variable: '--videoframe-badge-default-size' },
17
+ { label: 'icon color', element: 'badge', variable: '--videoframe-badge-default-icon' },
18
+ { label: 'icon size', canBeLinked: true, element: 'badge', variable: '--videoframe-badge-default-icon-size' },
19
+ { label: 'hover duration', element: 'badge', variable: '--videoframe-badge-duration' },
20
+ { label: 'hover easing', element: 'badge', variable: '--videoframe-badge-easing' }
21
+ ],
22
+ hover: [
23
+ { label: 'surface color', element: 'badge', variable: '--videoframe-badge-hover-surface' },
24
+ { label: 'border color', element: 'badge', variable: '--videoframe-badge-hover-border' },
25
+ { label: 'icon color', element: 'badge', variable: '--videoframe-badge-hover-icon' }
26
+ ]
27
+ };
28
+
29
+ export const allTokens: Token[] = Object.values(states).flat();
30
+ </script>
31
+
32
+ <script lang="ts">
33
+ import VideoFrame from '../../system/components/VideoFrame.svelte';
34
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
35
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
36
+ import clipUrl from '../../system/assets/watch-navigation.mp4';
37
+ import posterUrl from '../../system/assets/watch-navigation-poster.webp';
38
+ </script>
39
+
40
+ <ComponentEditorBase
41
+ {component}
42
+ title="Video Frame"
43
+ description="A poster frame that plays its clip in place when clicked."
44
+ tokens={allTokens}
45
+ >
46
+ <VariantGroup
47
+ name="videoframe"
48
+ title="Video Frame"
49
+ {states}
50
+ {component}
51
+ elementOrder={['frame', 'badge']}
52
+ >
53
+ {#snippet children({ activeState }: { activeState: string })}
54
+ <div class="videoframe-demo">
55
+ <VideoFrame
56
+ src={clipUrl}
57
+ poster={posterUrl}
58
+ alt="Navigation running on a smartwatch"
59
+ width={480}
60
+ height={552}
61
+ class={activeState === 'hover' ? 'force-hover' : ''}
62
+ />
63
+ </div>
64
+ {/snippet}
65
+ </VariantGroup>
66
+ </ComponentEditorBase>
67
+
68
+ <style>
69
+ .videoframe-demo {
70
+ width: 100%;
71
+ max-width: 14rem;
72
+ }
73
+ </style>
@@ -0,0 +1,95 @@
1
+ <script module lang="ts">
2
+ import type { Token } from './scaffolding/types';
3
+
4
+ export const component = 'videolightbox';
5
+
6
+ const states: Record<string, Token[]> = {
7
+ default: [
8
+ { label: 'surface color', element: 'tile', variable: '--videolightbox-tile-surface' },
9
+ { label: 'border color', element: 'tile', variable: '--videolightbox-tile-border' },
10
+ { label: 'border width', canBeLinked: true, element: 'tile', variable: '--videolightbox-tile-border-width' },
11
+ { label: 'corner radius', canBeLinked: true, element: 'tile', variable: '--videolightbox-tile-radius' },
12
+ { label: 'shadow', element: 'tile', variable: '--videolightbox-tile-shadow' },
13
+ { label: 'scrim color', element: 'overlay', variable: '--videolightbox-scrim-surface' },
14
+ { label: 'padding', canBeLinked: true, element: 'overlay', variable: '--videolightbox-overlay-padding' },
15
+ { label: 'fade duration', element: 'overlay', variable: '--videolightbox-overlay-duration' },
16
+ { label: 'fade easing', element: 'overlay', variable: '--videolightbox-overlay-easing' },
17
+ { label: 'surface color', element: 'close', variable: '--videolightbox-chrome-surface' },
18
+ { label: 'border color', element: 'close', variable: '--videolightbox-chrome-border' },
19
+ { label: 'border width', canBeLinked: true, element: 'close', variable: '--videolightbox-chrome-border-width' },
20
+ { label: 'corner radius', canBeLinked: true, element: 'close', variable: '--videolightbox-chrome-radius' },
21
+ { label: 'icon color', element: 'close', variable: '--videolightbox-chrome-icon' },
22
+ ],
23
+ hover: [
24
+ { label: 'surface color', element: 'close', variable: '--videolightbox-chrome-hover-surface' },
25
+ ],
26
+ };
27
+
28
+ export const allTokens: Token[] = Object.values(states).flat();
29
+ </script>
30
+
31
+ <script lang="ts">
32
+ import VideoLightbox from '../../system/components/VideoLightbox.svelte';
33
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
34
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
35
+ import clipUrl from '../../system/assets/watch-navigation.mp4';
36
+ import posterUrl from '../../system/assets/watch-navigation-poster.webp';
37
+ </script>
38
+
39
+ <ComponentEditorBase
40
+ {component}
41
+ title="Video Lightbox"
42
+ description="A small still that fades the page away and plays its clip in the middle of the screen. Click anywhere, the close button, or Escape to close it. The open preview beside the tile shows the overlay and close button in place."
43
+ tokens={allTokens}
44
+ >
45
+ <VariantGroup
46
+ name="videolightbox"
47
+ title="Video Lightbox"
48
+ {states}
49
+ {component}
50
+ elementOrder={['tile', 'overlay', 'close']}
51
+ >
52
+ {#snippet children({ activeState })}
53
+ <div class="videolightbox-demo">
54
+ <div class="videolightbox-demo-tile">
55
+ <VideoLightbox
56
+ src={clipUrl}
57
+ poster={posterUrl}
58
+ alt="Navigation running on a smartwatch"
59
+ width={480}
60
+ height={552}
61
+ />
62
+ </div>
63
+ <div class="videolightbox-demo-open">
64
+ <VideoLightbox
65
+ src={clipUrl}
66
+ poster={posterUrl}
67
+ width={480}
68
+ height={552}
69
+ inline
70
+ class={activeState === 'hover' ? 'force-hover' : ''}
71
+ />
72
+ </div>
73
+ </div>
74
+ {/snippet}
75
+ </VariantGroup>
76
+ </ComponentEditorBase>
77
+
78
+ <style>
79
+ .videolightbox-demo {
80
+ display: flex;
81
+ flex-wrap: wrap;
82
+ align-items: center;
83
+ gap: var(--ui-space-32);
84
+ width: 100%;
85
+ }
86
+
87
+ .videolightbox-demo-tile {
88
+ width: 8rem;
89
+ }
90
+
91
+ .videolightbox-demo-open {
92
+ flex: 1 1 18rem;
93
+ max-width: 28rem;
94
+ }
95
+ </style>
@@ -24,6 +24,12 @@ import ImageEditor, { allTokens as imageTokens, intrinsics as imageIntrinsics }
24
24
  import { catalogue as imageCatalogue } from '../../system/components/Image.svelte';
25
25
  import ImageLightboxEditor, { allTokens as imageLightboxTokens } from './ImageLightboxEditor.svelte';
26
26
  import { catalogue as imageLightboxCatalogue } from '../../system/components/ImageLightbox.svelte';
27
+ import VideoLightboxEditor, { allTokens as videoLightboxTokens } from './VideoLightboxEditor.svelte';
28
+ import VideoFrameEditor, { allTokens as videoFrameTokens } from './VideoFrameEditor.svelte';
29
+ import { catalogue as videoFrameCatalogue } from '../../system/components/VideoFrame.svelte';
30
+ import SlidePagerEditor, { allTokens as slidePagerTokens } from './SlidePagerEditor.svelte';
31
+ import { catalogue as slidePagerCatalogue } from '../../system/components/SlidePager.svelte';
32
+ import { catalogue as videoLightboxCatalogue } from '../../system/components/VideoLightbox.svelte';
27
33
  import InlineEditActionsEditor, { allTokens as inlineEditActionsTokens } from './InlineEditActionsEditor.svelte';
28
34
  import { catalogue as inlineEditActionsCatalogue } from '../../system/components/InlineEditActions.svelte';
29
35
  import InputEditor, { allTokens as inputTokens } from './InputEditor.svelte';
@@ -81,7 +87,10 @@ type BuiltInComponentId =
81
87
  | 'tabbar'
82
88
  | 'toggle'
83
89
  | 'tooltip'
84
- | 'progressbar';
90
+ | 'progressbar'
91
+ | 'videolightbox'
92
+ | 'videoframe'
93
+ | 'slidepager';
85
94
 
86
95
  /**
87
96
  * Public component id type. Widened to `string` because consumers can register
@@ -255,6 +264,36 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
255
264
  catalogue: imageLightboxCatalogue,
256
265
  origin: 'system',
257
266
  },
267
+ videolightbox: {
268
+ id: 'videolightbox',
269
+ label: 'Video Lightbox',
270
+ icon: 'fas fa-circle-play',
271
+ sourceFile: 'src/system/components/VideoLightbox.svelte',
272
+ editorComponent: VideoLightboxEditor,
273
+ schema: videoLightboxTokens,
274
+ catalogue: videoLightboxCatalogue,
275
+ origin: 'system',
276
+ },
277
+ videoframe: {
278
+ id: 'videoframe',
279
+ label: 'Video Frame',
280
+ icon: 'fas fa-film',
281
+ sourceFile: 'src/system/components/VideoFrame.svelte',
282
+ editorComponent: VideoFrameEditor,
283
+ schema: videoFrameTokens,
284
+ catalogue: videoFrameCatalogue,
285
+ origin: 'system',
286
+ },
287
+ slidepager: {
288
+ id: 'slidepager',
289
+ label: 'Slide Pager',
290
+ icon: 'fas fa-images',
291
+ sourceFile: 'src/system/components/SlidePager.svelte',
292
+ editorComponent: SlidePagerEditor,
293
+ schema: slidePagerTokens,
294
+ catalogue: slidePagerCatalogue,
295
+ origin: 'system',
296
+ },
258
297
  inlineeditactions: {
259
298
  id: 'inlineeditactions',
260
299
  label: 'Inline Edit Actions',
@@ -192,6 +192,13 @@ const PART_SPECS: readonly PartSpec[] = [
192
192
  // Close and the two nav chevrons all carry this class; each is fixed and
193
193
  // floats over the photo rather than a scrim of its own.
194
194
  { sel: '.image-lightbox-chrome', stem: 'imagelightbox-chrome', positioned: true, unmasked: true },
195
+ // The trigger inside clips the still, so the tile itself stays unclipped.
196
+ { sel: '.videolightbox', stem: 'videolightbox-tile' },
197
+ {
198
+ sel: '.videolightbox-modal', fill: 'var(--videolightbox-scrim-surface)',
199
+ stroke: 'transparent', positioned: true, unmasked: true,
200
+ },
201
+ { sel: '.videolightbox-close', stem: 'videolightbox-chrome', positioned: true, unmasked: true },
195
202
 
196
203
  // Status blocks
197
204
  ...STATUS_VARIANTS.map((v) => ({ sel: `.callout-${v}`, stem: `callout-${v}` })),
@@ -47,6 +47,8 @@
47
47
  projectRoot = INJECTED_PROJECT_ROOT
48
48
  }: Props = $props();
49
49
 
50
+ let activeLink = $derived(navLinks.find((l) => l.path === $route));
51
+
50
52
  // Dev-only; skip inside iframe (editor route embeds this app).
51
53
  const isDev = import.meta.env.DEV;
52
54
  const isInIframe = typeof window !== 'undefined' && window.parent !== window;
@@ -677,22 +679,19 @@
677
679
 
678
680
  {#if open && navLinks.length > 0}
679
681
  <div class="seg-group" transition:fade={BTN_FADE}>
680
- <span class="seg-label">Active Page:</span>
681
- <div class="seg-bar" role="tablist" aria-label="Underlying page">
682
- {#each navLinks as link (link.path)}
683
- <button
684
- type="button"
685
- role="tab"
686
- class="seg-pill"
687
- class:active={$route === link.path}
688
- aria-selected={$route === link.path}
689
- disabled={link.disabled}
690
- onclick={() => navigate(link.path)}
691
- >
692
- {#if link.icon}<i class="fas {link.icon}"></i>{/if}
693
- <span>{link.label}</span>
694
- </button>
695
- {/each}
682
+ <label class="seg-label" for="lt-active-page">Active Page:</label>
683
+ <div class="page-select">
684
+ {#if activeLink?.icon}<i class="fas {activeLink.icon} page-icon"></i>{/if}
685
+ <select
686
+ id="lt-active-page"
687
+ value={$route}
688
+ onchange={(e) => navigate(e.currentTarget.value)}
689
+ >
690
+ {#each navLinks as link (link.path)}
691
+ <option value={link.path} disabled={link.disabled}>{link.label}</option>
692
+ {/each}
693
+ </select>
694
+ <i class="fas fa-chevron-down page-caret"></i>
696
695
  </div>
697
696
  </div>
698
697
  {/if}
@@ -1116,61 +1115,55 @@
1116
1115
  color: var(--ui-text-primary, #fff);
1117
1116
  }
1118
1117
 
1119
- .seg-bar {
1118
+ .page-select {
1119
+ position: relative;
1120
1120
  display: inline-flex;
1121
1121
  align-items: center;
1122
- gap: var(--ui-space-4, 4px);
1123
- padding: 3px;
1122
+ }
1123
+
1124
+ .page-select select {
1125
+ appearance: none;
1126
+ color-scheme: dark;
1127
+ padding: var(--ui-space-4, 4px) calc(var(--ui-space-8, 8px) + 20px) var(--ui-space-4, 4px) var(--ui-space-8, 8px);
1124
1128
  background: rgba(0, 0, 0, 0.55);
1125
1129
  border: 1px solid rgba(255, 255, 255, 0.28);
1126
1130
  border-radius: var(--ui-radius-lg, 6px);
1127
- box-shadow:
1128
- inset 0 1px 0 rgba(0, 0, 0, 0.5),
1129
- 0 0 0 1px rgba(0, 0, 0, 0.4);
1130
- }
1131
-
1132
- .seg-pill {
1133
- display: inline-flex;
1134
- align-items: center;
1135
- gap: var(--ui-space-4, 4px);
1136
- padding: var(--ui-space-4, 4px) var(--ui-space-8, 8px);
1137
- background: transparent;
1138
- border: 1px solid transparent;
1139
- border-radius: 3px;
1140
- color: rgba(255, 255, 255, 0.6);
1131
+ color: var(--ui-text-primary, #fff);
1141
1132
  font-family: inherit;
1142
1133
  font-size: var(--ui-font-size-md, 16px);
1143
1134
  font-weight: var(--ui-font-weight-medium, 500);
1144
1135
  cursor: pointer;
1145
- transition:
1146
- background var(--ui-transition-fast, 120ms ease),
1147
- color var(--ui-transition-fast, 120ms ease),
1148
- border-color var(--ui-transition-fast, 120ms ease);
1136
+ transition: border-color var(--ui-transition-fast, 120ms ease);
1149
1137
  }
1150
1138
 
1151
- .seg-pill i {
1152
- font-size: var(--ui-font-size-md, 16px);
1153
- opacity: 0.85;
1139
+ .page-icon ~ select {
1140
+ padding-left: calc(var(--ui-space-8, 8px) + 24px);
1154
1141
  }
1155
1142
 
1156
- .seg-pill:hover:not(:disabled) {
1157
- color: rgba(255, 255, 255, 0.9);
1143
+ .page-select select:hover {
1144
+ border-color: rgba(255, 255, 255, 0.5);
1158
1145
  }
1159
1146
 
1160
- .seg-pill:disabled {
1161
- color: rgba(255, 255, 255, 0.28);
1162
- cursor: not-allowed;
1147
+ .page-select select:focus-visible {
1148
+ outline: 2px solid rgba(255, 255, 255, 0.5);
1149
+ outline-offset: 2px;
1163
1150
  }
1164
1151
 
1165
- .seg-pill:disabled i {
1166
- opacity: 0.5;
1152
+ .page-icon,
1153
+ .page-caret {
1154
+ position: absolute;
1155
+ pointer-events: none;
1156
+ color: rgba(255, 255, 255, 0.6);
1167
1157
  }
1168
1158
 
1169
- /* Outlined (not filled) so this reads as sibling to iframe's switcher, not a twin. */
1170
- .seg-pill.active {
1171
- color: var(--ui-text-primary, #fff);
1172
- border-color: rgba(255, 255, 255, 0.5);
1173
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.25) 100%);
1159
+ .page-icon {
1160
+ left: var(--ui-space-8, 8px);
1161
+ font-size: var(--ui-font-size-md, 16px);
1162
+ }
1163
+
1164
+ .page-caret {
1165
+ right: var(--ui-space-8, 8px);
1166
+ font-size: var(--ui-font-size-xs, 12px);
1174
1167
  }
1175
1168
 
1176
1169
  .frame-wrap {
@@ -193,7 +193,7 @@
193
193
  </div>
194
194
  {#if drawerOpen}
195
195
  <div class="sidebar-footer">
196
- <ThemePanel showComponentsLink={false} />
196
+ <ThemePanel />
197
197
  </div>
198
198
  {/if}
199
199
  </nav>