@motion-proto/live-tokens 0.84.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 (87) hide show
  1. package/.claude/skills/live-tokens-create-component/references/token-naming.md +0 -1
  2. package/CHANGELOG.md +85 -0
  3. package/README.md +1 -1
  4. package/dist-plugin/{chunk-V2OVSC5Z.js → chunk-GWMEYBZ2.js} +22 -1
  5. package/dist-plugin/{chunk-FD4FVDOC.js → chunk-ONYAIXCZ.js} +74 -1
  6. package/dist-plugin/{chunk-M6BWSS6C.js → chunk-W5VQV32K.js} +1 -1
  7. package/dist-plugin/index.cjs +101 -7
  8. package/dist-plugin/index.js +3 -3
  9. package/dist-plugin/migrateData/index.cjs +78 -5
  10. package/dist-plugin/migrateData/index.js +2 -2
  11. package/dist-plugin/setColors/index.cjs +74 -1
  12. package/dist-plugin/setColors/index.js +1 -1
  13. package/dist-plugin/setGeometry/index.cjs +74 -4
  14. package/dist-plugin/setGeometry/index.d.cts +1 -1
  15. package/dist-plugin/setGeometry/index.d.ts +1 -1
  16. package/dist-plugin/setGeometry/index.js +1 -4
  17. package/dist-plugin/tokensCssMigrations/index.cjs +22 -1
  18. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  19. package/package.json +1 -1
  20. package/src/editor/component-editor/DialogEditor.svelte +1 -2
  21. package/src/editor/component-editor/ImageLightboxEditor.svelte +1 -2
  22. package/src/editor/component-editor/SlidePagerEditor.svelte +118 -0
  23. package/src/editor/component-editor/VideoFrameEditor.svelte +73 -0
  24. package/src/editor/component-editor/VideoLightboxEditor.svelte +95 -0
  25. package/src/editor/component-editor/registry.ts +40 -1
  26. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -4
  27. package/src/editor/core/components/aliasKinds.ts +0 -4
  28. package/src/editor/core/sketch/sketchLayer.ts +8 -1
  29. package/src/editor/core/store/editorPersistence.ts +46 -23
  30. package/src/editor/core/store/editorStore.ts +1 -5
  31. package/src/editor/core/store/editorTypes.ts +11 -7
  32. package/src/editor/core/themes/migrations/2026-09-21-scrim-surface.ts +56 -0
  33. package/src/editor/core/themes/migrations/2026-09-21-wash-color-and-opacity.ts +51 -0
  34. package/src/editor/core/themes/migrations/index.ts +4 -0
  35. package/src/editor/core/themes/slices/washes.ts +59 -77
  36. package/src/editor/overlay/LiveEditorOverlay.svelte +45 -52
  37. package/src/editor/pages/ComponentEditorPage.svelte +1 -1
  38. package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
  39. package/src/editor/ui/ThemePanel.svelte +9 -53
  40. package/src/editor/ui/UIPaletteSelector.svelte +5 -1
  41. package/src/editor/ui/UIVariantSelector.svelte +1 -1
  42. package/src/editor/ui/sections/WashesSection.svelte +100 -99
  43. package/src/editor/ui/variantScales.ts +0 -11
  44. package/src/live-tokens/data/colors-and-type/autumn.json +9 -7
  45. package/src/live-tokens/data/colors-and-type/default.json +9 -7
  46. package/src/live-tokens/data/colors-and-type/halloween.json +9 -7
  47. package/src/live-tokens/data/colors-and-type/midnight-study.json +9 -7
  48. package/src/live-tokens/data/colors-and-type/ocean.json +9 -7
  49. package/src/live-tokens/data/colors-and-type/royal-velvet.json +9 -7
  50. package/src/live-tokens/data/colors-and-type/sketchy.json +9 -7
  51. package/src/live-tokens/data/colors-and-type/spring-meadow.json +9 -7
  52. package/src/live-tokens/data/colors-and-type/sunset.json +9 -7
  53. package/src/live-tokens/data/themes/autumn.json +98 -12
  54. package/src/live-tokens/data/themes/halloween.json +98 -12
  55. package/src/live-tokens/data/themes/midnight-study.json +98 -12
  56. package/src/live-tokens/data/themes/ocean.json +98 -12
  57. package/src/live-tokens/data/themes/royal-velvet.json +98 -12
  58. package/src/live-tokens/data/themes/sketchy.json +98 -12
  59. package/src/live-tokens/data/themes/spring-meadow.json +98 -12
  60. package/src/live-tokens/data/themes/sunset.json +98 -12
  61. package/src/live-tokens/data/tokens.generated.css +8 -6
  62. package/src/system/assets/slide-04.webp +0 -0
  63. package/src/system/assets/slide-05.webp +0 -0
  64. package/src/system/assets/slide-06.webp +0 -0
  65. package/src/system/assets/watch-navigation-poster.webp +0 -0
  66. package/src/system/assets/watch-navigation.mp4 +0 -0
  67. package/src/system/components/Dialog.svelte +2 -7
  68. package/src/system/components/ImageLightbox.svelte +27 -39
  69. package/src/system/components/SlidePager.svelte +516 -0
  70. package/src/system/components/VideoFrame.svelte +284 -0
  71. package/src/system/components/VideoLightbox.svelte +381 -0
  72. package/src/system/styles/tokens.css +13 -8
  73. package/src/testing-js/{chunk-WQJ6QB6Y.js → chunk-OHU5BHGZ.js} +347 -9
  74. package/src/testing-js/chunk-OHU5BHGZ.js.map +1 -0
  75. package/src/testing-js/{chunk-XXABJNUU.js → chunk-Y6R7Z7VK.js} +10 -6
  76. package/src/testing-js/chunk-Y6R7Z7VK.js.map +1 -0
  77. package/src/testing-js/component-behavior.contract.js +1 -1
  78. package/src/testing-js/component-editor.contract.js +1 -1
  79. package/src/testing-js/component-render.contract.js +1 -1
  80. package/src/testing-js/index.d.ts +2 -2
  81. package/src/testing-js/index.js +2 -2
  82. package/src/testing-js/page-compliance.contract.js +1 -1
  83. package/src/testing-js/{vitest-BMLIbDs2.d.ts → vitest-5vWtqwWe.d.ts} +2 -2
  84. package/src/testing-js/vitest.d.ts +1 -1
  85. package/src/testing-js/vitest.js +2 -2
  86. package/src/testing-js/chunk-WQJ6QB6Y.js.map +0 -1
  87. package/src/testing-js/chunk-XXABJNUU.js.map +0 -1
@@ -74,9 +74,6 @@ var KIND_RULES = [
74
74
  { kind: "dot-size", suffix: ["-dot-size"] },
75
75
  { kind: "blur", suffix: ["-blur"], prefix: ["--blur-"] },
76
76
  { kind: "scale", suffix: ["-scale"], prefix: ["--scale-"] },
77
- // A strength from 0 to 1 that the component mixes into a colour where it
78
- // paints. A palette picker here writes a colour into a number.
79
- { kind: "opacity", suffix: ["-opacity"], prefix: ["--scrim-opacity"] },
80
77
  { kind: "shadow", suffix: ["-shadow"], prefix: ["--shadow-"] },
81
78
  { kind: "padding", suffix: ["-padding", "-margin"] },
82
79
  { kind: "gap", suffix: ["-gap"] },
@@ -1569,6 +1566,77 @@ var componentMigration_2026_09_20_scrimColorAndOpacity = {
1569
1566
  }
1570
1567
  };
1571
1568
 
1569
+ // src/editor/core/themes/migrations/2026-09-21-wash-color-and-opacity.ts
1570
+ var FAMILIES = ["scrim", "tint"];
1571
+ var COLOR_MIX_RE = /^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+(?:\.\d+)?)%,\s*transparent\)$/i;
1572
+ var PLAIN_VAR_RE = /^var\((--[a-z0-9-]+)\)$/i;
1573
+ function parseFill(raw) {
1574
+ const s = raw.trim();
1575
+ const mix = s.match(COLOR_MIX_RE);
1576
+ if (mix) return { color: mix[1], opacity: Math.min(100, Number(mix[2])) / 100 };
1577
+ const plain = s.match(PLAIN_VAR_RE);
1578
+ return plain ? { color: plain[1], opacity: 1 } : null;
1579
+ }
1580
+ var colorsAndTypeMigration_2026_09_21_washColorAndOpacity = {
1581
+ id: "2026-09-21-wash-color-and-opacity",
1582
+ fromVersion: 8,
1583
+ toVersion: 9,
1584
+ appliesTo: "colors-and-type",
1585
+ apply(rawVars) {
1586
+ const out = { ...rawVars };
1587
+ for (const family of FAMILIES) {
1588
+ let color = null;
1589
+ for (const suffix of ["", "-low", "-high"]) {
1590
+ const key = `--${family}${suffix}`;
1591
+ const parsed = out[key] === void 0 ? null : parseFill(out[key]);
1592
+ if (!parsed) continue;
1593
+ color ??= parsed.color;
1594
+ out[`--${family}-opacity${suffix}`] = String(parsed.opacity);
1595
+ delete out[key];
1596
+ }
1597
+ if (color) out[`--${family}-color`] = `var(${color})`;
1598
+ }
1599
+ return out;
1600
+ }
1601
+ };
1602
+
1603
+ // src/editor/core/themes/migrations/2026-09-21-scrim-surface.ts
1604
+ var STOPS2 = {
1605
+ "--scrim-opacity-low": "--scrim-low",
1606
+ "--scrim-opacity": "--scrim",
1607
+ "--scrim-opacity-high": "--scrim-high"
1608
+ };
1609
+ var PAIR = /^--(imagelightbox|dialog)-scrim-(color|opacity)$/;
1610
+ var ref = (value) => value.startsWith("--") ? `var(${value})` : value;
1611
+ function fill(color, opacity) {
1612
+ if (color === "--scrim-color" && STOPS2[opacity]) return STOPS2[opacity];
1613
+ if (opacity === "1") return color;
1614
+ const pct = Number.isFinite(Number(opacity)) ? `${Number(opacity) * 100}%` : `calc(${ref(opacity)} * 100%)`;
1615
+ return `color-mix(in srgb, ${ref(color)} ${pct}, transparent)`;
1616
+ }
1617
+ var componentMigration_2026_09_21_scrimSurface = {
1618
+ id: "2026-09-21-scrim-surface",
1619
+ fromVersion: 37,
1620
+ toVersion: 38,
1621
+ appliesTo: "component-config",
1622
+ apply(rawVars) {
1623
+ const out = {};
1624
+ const pairs = {};
1625
+ for (const [key, value] of Object.entries(rawVars)) {
1626
+ const match = PAIR.exec(key);
1627
+ if (!match) {
1628
+ out[key] = value;
1629
+ continue;
1630
+ }
1631
+ (pairs[match[1]] ??= {})[match[2]] = value;
1632
+ }
1633
+ for (const [id, { color = "--scrim-color", opacity = "--scrim-opacity-high" }] of Object.entries(pairs)) {
1634
+ out[`--${id}-scrim-surface`] = fill(color, opacity);
1635
+ }
1636
+ return out;
1637
+ }
1638
+ };
1639
+
1572
1640
  // src/editor/core/themes/migrations/2026-09-13-cornerbadge-prefix.ts
1573
1641
  var RENAMES8 = {
1574
1642
  cornerbadge: {
@@ -1765,7 +1833,9 @@ var MIGRATIONS = [
1765
1833
  componentMigration_2026_09_13_collapsiblesectionOpen,
1766
1834
  componentMigration_2026_09_13_badgeBrand,
1767
1835
  componentMigration_2026_09_20_imagelightboxScrim,
1768
- componentMigration_2026_09_20_scrimColorAndOpacity
1836
+ componentMigration_2026_09_20_scrimColorAndOpacity,
1837
+ colorsAndTypeMigration_2026_09_21_washColorAndOpacity,
1838
+ componentMigration_2026_09_21_scrimSurface
1769
1839
  ];
1770
1840
  function countFor(kind) {
1771
1841
  return MIGRATIONS.filter((m) => m.appliesTo === kind).length;
@@ -37,7 +37,7 @@ interface AdjustResult {
37
37
  }
38
38
  declare function adjustAliases(configs: Record<string, ComponentConfig>, ops: readonly AdjustOp[], now: string): AdjustResult;
39
39
 
40
- type TokenKind = 'surface' | 'border' | 'border-width' | 'hairline-width' | 'indicator-width' | 'radius' | 'track-height' | 'hairline-inset' | 'dot-size' | 'length' | 'blur' | 'scale' | 'opacity' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
40
+ type TokenKind = 'surface' | 'border' | 'border-width' | 'hairline-width' | 'indicator-width' | 'radius' | 'track-height' | 'hairline-inset' | 'dot-size' | 'length' | 'blur' | 'scale' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
41
41
  /** Side-aware kind test. Per-side paddings (`--card-default-body-padding-top`)
42
42
  are written by UIPaddingSelector and never declared as editor tokens, so
43
43
  `rawKind` never meets one; the CLI does, and they belong with their parent. */
@@ -37,7 +37,7 @@ interface AdjustResult {
37
37
  }
38
38
  declare function adjustAliases(configs: Record<string, ComponentConfig>, ops: readonly AdjustOp[], now: string): AdjustResult;
39
39
 
40
- type TokenKind = 'surface' | 'border' | 'border-width' | 'hairline-width' | 'indicator-width' | 'radius' | 'track-height' | 'hairline-inset' | 'dot-size' | 'length' | 'blur' | 'scale' | 'opacity' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
40
+ type TokenKind = 'surface' | 'border' | 'border-width' | 'hairline-width' | 'indicator-width' | 'radius' | 'track-height' | 'hairline-inset' | 'dot-size' | 'length' | 'blur' | 'scale' | 'shadow' | 'font-family' | 'font-weight' | 'font-size' | 'line-height' | 'letter-spacing' | 'padding' | 'gap' | 'duration' | 'easing' | 'text-color';
41
41
  /** Side-aware kind test. Per-side paddings (`--card-default-body-padding-top`)
42
42
  are written by UIPaddingSelector and never declared as editor tokens, so
43
43
  `rawKind` never meets one; the CLI does, and they belong with their parent. */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CURRENT_COMPONENT_SCHEMA_VERSION
3
- } from "../chunk-FD4FVDOC.js";
3
+ } from "../chunk-ONYAIXCZ.js";
4
4
  import {
5
5
  readLiveTokensConfig,
6
6
  resolveDataDirs
@@ -42,9 +42,6 @@ var KIND_RULES = [
42
42
  { kind: "dot-size", suffix: ["-dot-size"] },
43
43
  { kind: "blur", suffix: ["-blur"], prefix: ["--blur-"] },
44
44
  { kind: "scale", suffix: ["-scale"], prefix: ["--scale-"] },
45
- // A strength from 0 to 1 that the component mixes into a colour where it
46
- // paints. A palette picker here writes a colour into a number.
47
- { kind: "opacity", suffix: ["-opacity"], prefix: ["--scrim-opacity"] },
48
45
  { kind: "shadow", suffix: ["-shadow"], prefix: ["--shadow-"] },
49
46
  { kind: "padding", suffix: ["-padding", "-margin"] },
50
47
  { kind: "gap", suffix: ["-gap"] },
@@ -659,6 +659,26 @@ var tokensCssMigration_2026_09_20_scrimColorAndOpacity = {
659
659
  }
660
660
  };
661
661
 
662
+ // vite-plugin/tokensCssMigrations/migrations/2026-09-21-tint-color-and-opacity.ts
663
+ var ENTRIES3 = [
664
+ { name: "--tint-color", value: "rgb(255, 255, 255)" },
665
+ { name: "--tint-opacity-low", value: "0.05" },
666
+ { name: "--tint-opacity", value: "0.1" },
667
+ { name: "--tint-opacity-high", value: "0.15" }
668
+ ];
669
+ var tokensCssMigration_2026_09_21_tintColorAndOpacity = {
670
+ id: "2026-09-21-tint-color-and-opacity",
671
+ kind: "additive",
672
+ description: "Add --tint-color and --tint-opacity-*; a tint's colour and strength are separate tokens",
673
+ apply(css) {
674
+ return ensureScale(css, {
675
+ entries: ENTRIES3,
676
+ sectionComment: "Tint colour and strength, which the --tint-* stops compose",
677
+ anchorPrefixes: ["--tint"]
678
+ });
679
+ }
680
+ };
681
+
662
682
  // vite-plugin/files/dataPaths.ts
663
683
  var import_fs = __toESM(require("fs"), 1);
664
684
  var import_path = __toESM(require("path"), 1);
@@ -712,7 +732,8 @@ var TOKENS_CSS_MIGRATIONS = [
712
732
  tokensCssMigration_2026_08_27_editorialLargeSteps,
713
733
  tokensCssMigration_2026_09_01_scrimRename,
714
734
  tokensCssMigration_2026_09_01_tintScale,
715
- tokensCssMigration_2026_09_20_scrimColorAndOpacity
735
+ tokensCssMigration_2026_09_20_scrimColorAndOpacity,
736
+ tokensCssMigration_2026_09_21_tintColorAndOpacity
716
737
  ];
717
738
  function runTokensCssMigrations(css) {
718
739
  return foldMigrations(css, () => true);
@@ -13,7 +13,7 @@ import {
13
13
  runTokensCssMigrations,
14
14
  semverBumpType,
15
15
  validateTokensCss
16
- } from "../chunk-V2OVSC5Z.js";
16
+ } from "../chunk-GWMEYBZ2.js";
17
17
  import "../chunk-7TQQTHI6.js";
18
18
  import {
19
19
  readLiveTokensConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.84.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": [
@@ -16,8 +16,7 @@
16
16
  // Button styling lives in Button.svelte — the dialog only owns its own chrome.
17
17
  const frameStates: Record<string, Token[]> = {
18
18
  scrim: [
19
- { label: 'scrim color', groupKey: 'surface', variable: '--dialog-scrim-color' },
20
- { label: 'scrim opacity', variable: '--dialog-scrim-opacity' },
19
+ { label: 'scrim color', groupKey: 'surface', variable: '--dialog-scrim-surface' },
21
20
  ],
22
21
  dialog: [
23
22
  { label: 'surface color', variable: '--dialog-surface' },
@@ -15,8 +15,7 @@
15
15
  { label: 'tile shadow', variable: '--imagelightbox-tile-shadow' },
16
16
  ],
17
17
  overlay: [
18
- { label: 'scrim color', groupKey: 'surface', variable: '--imagelightbox-scrim-color' },
19
- { label: 'scrim opacity', variable: '--imagelightbox-scrim-opacity' },
18
+ { label: 'scrim color', groupKey: 'surface', variable: '--imagelightbox-scrim-surface' },
20
19
  ],
21
20
  chrome: [
22
21
  { label: 'surface color', groupKey: 'surface', variable: '--imagelightbox-chrome-surface' },
@@ -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',
@@ -9,7 +9,7 @@
9
9
  import UIPaddingSelector from '../../ui/UIPaddingSelector.svelte';
10
10
  import UILetterSpacingSelector from '../../ui/UILetterSpacingSelector.svelte';
11
11
  import UIEasingSelector from '../../ui/UIEasingSelector.svelte';
12
- import { BLUR, BORDER_WIDTH, DOT_SIZE, DURATION, RADIUS, SCALE, SCRIM_OPACITY, SHADOW, TRACK_HEIGHT, HAIRLINE_INSET } from '../../ui/variantScales';
12
+ import { BLUR, BORDER_WIDTH, DOT_SIZE, DURATION, RADIUS, SCALE, SHADOW, TRACK_HEIGHT, HAIRLINE_INSET } from '../../ui/variantScales';
13
13
  import {
14
14
  editorState,
15
15
  getComponentPropertySiblings,
@@ -80,7 +80,6 @@
80
80
  'easing',
81
81
  'blur',
82
82
  'scale',
83
- 'opacity',
84
83
  'shadow',
85
84
  'text-color',
86
85
  'surface',
@@ -161,7 +160,6 @@
161
160
  'easing': { component: UIEasingSelector },
162
161
  'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
163
162
  'scale': { component: UIVariantSelector, extra: () => ({ ...SCALE }) },
164
- 'opacity': { component: UIVariantSelector, extra: () => ({ ...SCRIM_OPACITY }) },
165
163
  'shadow': { component: UIVariantSelector, extra: () => ({ ...SHADOW }) },
166
164
  'surface': { component: UIPaletteSelector, extra: paletteExtra },
167
165
  'border': { component: UIPaletteSelector, extra: paletteExtra },
@@ -192,7 +190,6 @@
192
190
  'easing',
193
191
  'blur',
194
192
  'scale',
195
- 'opacity',
196
193
  'shadow',
197
194
  'surface',
198
195
  'border-width',
@@ -11,7 +11,6 @@ export type TokenKind =
11
11
  | 'length'
12
12
  | 'blur'
13
13
  | 'scale'
14
- | 'opacity'
15
14
  | 'shadow'
16
15
  | 'font-family'
17
16
  | 'font-weight'
@@ -60,9 +59,6 @@ export const KIND_RULES: ReadonlyArray<{
60
59
  { kind: 'dot-size', suffix: ['-dot-size'] },
61
60
  { kind: 'blur', suffix: ['-blur'], prefix: ['--blur-'] },
62
61
  { kind: 'scale', suffix: ['-scale'], prefix: ['--scale-'] },
63
- // A strength from 0 to 1 that the component mixes into a colour where it
64
- // paints. A palette picker here writes a colour into a number.
65
- { kind: 'opacity', suffix: ['-opacity'], prefix: ['--scrim-opacity'] },
66
62
  { kind: 'shadow', suffix: ['-shadow'], prefix: ['--shadow-'] },
67
63
  { kind: 'padding', suffix: ['-padding', '-margin'] },
68
64
  { kind: 'gap', suffix: ['-gap'] },
@@ -186,12 +186,19 @@ const PART_SPECS: readonly PartSpec[] = [
186
186
  { sel: '.image-lightbox-thumb', stem: 'imagelightbox-tile', positioned: true, clips: true },
187
187
  // A fixed full-viewport scrim behind the open modal; no border of its own.
188
188
  {
189
- sel: '.image-lightbox-overlay', fill: 'color-mix(in srgb, var(--imagelightbox-scrim-color) calc(var(--imagelightbox-scrim-opacity) * 100%), transparent)',
189
+ sel: '.image-lightbox-overlay', fill: 'var(--imagelightbox-scrim-surface)',
190
190
  stroke: 'transparent', positioned: true, unmasked: true,
191
191
  },
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}` })),