@ds-gn/ui 0.7.0 → 0.9.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.
@@ -1505,6 +1505,30 @@
1505
1505
  "privacy": "private",
1506
1506
  "default": "new AutoRotateController( this, DEFAULT_DURATION, () => this._inView.value === true, )"
1507
1507
  },
1508
+ {
1509
+ "kind": "field",
1510
+ "name": "_motion",
1511
+ "privacy": "private",
1512
+ "default": "new MotionController(this)"
1513
+ },
1514
+ {
1515
+ "kind": "field",
1516
+ "name": "_shownIndex",
1517
+ "type": {
1518
+ "text": "number | undefined"
1519
+ },
1520
+ "privacy": "private",
1521
+ "description": "The feature whose detail last faded in; the fade replays when it changes."
1522
+ },
1523
+ {
1524
+ "kind": "field",
1525
+ "name": "_motionState",
1526
+ "type": {
1527
+ "text": "'static' | 'held' | 'played' | undefined"
1528
+ },
1529
+ "privacy": "private",
1530
+ "description": "Motion progress, decided by the first visibility report."
1531
+ },
1508
1532
  {
1509
1533
  "kind": "field",
1510
1534
  "name": "surface",
@@ -1591,6 +1615,38 @@
1591
1615
  "description": "Accessible label for the tab list.",
1592
1616
  "attribute": "tabs-aria-label"
1593
1617
  },
1618
+ {
1619
+ "kind": "field",
1620
+ "name": "motion",
1621
+ "type": {
1622
+ "text": "MotionType"
1623
+ },
1624
+ "description": "Animate the section in when it scrolls into view: the header's heading\nletter by letter, then its description, then the tabs as a whole. The\neyebrow stays static. A section already in view when it loads, and\nreduced motion, stay static.",
1625
+ "attribute": "motion",
1626
+ "reflects": true
1627
+ },
1628
+ {
1629
+ "kind": "method",
1630
+ "name": "_updateMotion",
1631
+ "privacy": "private",
1632
+ "description": "Decide the motion from the first visibility report: a section already\non screen stays static; one off screen holds its parts hidden (from\nscript, so nothing is hidden when scripts do not run) and plays when it\ncomes into view, so even a jump to it shows no unanimated frame."
1633
+ },
1634
+ {
1635
+ "kind": "method",
1636
+ "name": "_motionParts",
1637
+ "privacy": "private"
1638
+ },
1639
+ {
1640
+ "kind": "method",
1641
+ "name": "_playMotion",
1642
+ "privacy": "private"
1643
+ },
1644
+ {
1645
+ "kind": "method",
1646
+ "name": "_fadeInDetail",
1647
+ "privacy": "private",
1648
+ "description": "Fade the detail in whenever the active feature changes. `keyed` renders a\nfresh element per feature, so each change animates a new node. This runs\nafter render and before paint, so the new detail never shows unfaded."
1649
+ },
1594
1650
  {
1595
1651
  "kind": "method",
1596
1652
  "name": "_handleTabClick",
@@ -1841,6 +1897,14 @@
1841
1897
  },
1842
1898
  "default": "DEFAULT_TABS_ARIA_LABEL",
1843
1899
  "description": "Accessible label for the tab list."
1900
+ },
1901
+ {
1902
+ "name": "motion",
1903
+ "fieldName": "motion",
1904
+ "type": {
1905
+ "text": "MotionType"
1906
+ },
1907
+ "description": "Animate the section in when it scrolls into view: the header's heading\nletter by letter, then its description, then the tabs as a whole. The\neyebrow stays static. A section already in view when it loads, and\nreduced motion, stay static."
1844
1908
  }
1845
1909
  ]
1846
1910
  }
@@ -2621,6 +2685,32 @@
2621
2685
  "description": "Space below the section. Unset follows the inherited section space.",
2622
2686
  "attribute": "padding-bottom",
2623
2687
  "reflects": true
2688
+ },
2689
+ {
2690
+ "kind": "field",
2691
+ "name": "motion",
2692
+ "type": {
2693
+ "text": "MotionType"
2694
+ },
2695
+ "description": "Animate the content's eyebrow in letter by letter once the hero loads,\neach letter fading in with a small rise.\nEverything else stays static, so the hero's largest text paints at once.\nUnder reduced motion the eyebrow is static too.",
2696
+ "attribute": "motion",
2697
+ "reflects": true
2698
+ },
2699
+ {
2700
+ "kind": "field",
2701
+ "name": "_motion",
2702
+ "privacy": "private",
2703
+ "default": "new MotionController(this)"
2704
+ },
2705
+ {
2706
+ "kind": "method",
2707
+ "name": "_eyebrows",
2708
+ "privacy": "private",
2709
+ "return": {
2710
+ "type": {
2711
+ "text": "Element[]"
2712
+ }
2713
+ }
2624
2714
  }
2625
2715
  ],
2626
2716
  "superclass": {
@@ -2677,6 +2767,14 @@
2677
2767
  "text": "SectionSpaceType | undefined"
2678
2768
  },
2679
2769
  "description": "Space below the section. Unset follows the inherited section space."
2770
+ },
2771
+ {
2772
+ "name": "motion",
2773
+ "fieldName": "motion",
2774
+ "type": {
2775
+ "text": "MotionType"
2776
+ },
2777
+ "description": "Animate the content's eyebrow in letter by letter once the hero loads,\neach letter fading in with a small rise.\nEverything else stays static, so the hero's largest text paints at once.\nUnder reduced motion the eyebrow is static too."
2680
2778
  }
2681
2779
  ]
2682
2780
  }
@@ -3620,6 +3718,13 @@
3620
3718
  "attribute": "no-escape-dismiss",
3621
3719
  "reflects": true
3622
3720
  },
3721
+ {
3722
+ "kind": "field",
3723
+ "name": "_motion",
3724
+ "privacy": "private",
3725
+ "readonly": true,
3726
+ "default": "new MotionController(this)"
3727
+ },
3623
3728
  {
3624
3729
  "kind": "field",
3625
3730
  "name": "_closing",
@@ -3627,8 +3732,17 @@
3627
3732
  "text": "boolean"
3628
3733
  },
3629
3734
  "privacy": "private",
3630
- "default": "false",
3631
- "description": "Drives `data-state`, which the stylesheet keys the exit animation on."
3735
+ "default": "false"
3736
+ },
3737
+ {
3738
+ "kind": "field",
3739
+ "name": "_closeRun",
3740
+ "type": {
3741
+ "text": "number"
3742
+ },
3743
+ "privacy": "private",
3744
+ "default": "0",
3745
+ "description": "Invalidates an exit when reopened, disconnected, or closed externally."
3632
3746
  },
3633
3747
  {
3634
3748
  "kind": "field",
@@ -3724,7 +3838,7 @@
3724
3838
  "kind": "field",
3725
3839
  "name": "_motionOrigin",
3726
3840
  "type": {
3727
- "text": "string"
3841
+ "text": "Exclude<ModalMotionType, 'inherit'>"
3728
3842
  },
3729
3843
  "privacy": "private",
3730
3844
  "description": "The resolved animation origin, with `inherit` folded into `placement`.",
@@ -3800,6 +3914,30 @@
3800
3914
  ],
3801
3915
  "description": "Play the exit animation, then leave the top layer. The dialog stays in the\ntop layer for the whole animation because `dialog.close()` is what removes\nit, and that is deliberately the last step — no timers involved, the wait is\non the animations' own `finished` promises."
3802
3916
  },
3917
+ {
3918
+ "kind": "method",
3919
+ "name": "_animateDialog",
3920
+ "privacy": "private",
3921
+ "return": {
3922
+ "type": {
3923
+ "text": "Promise<void>"
3924
+ }
3925
+ },
3926
+ "parameters": [
3927
+ {
3928
+ "name": "dialog",
3929
+ "type": {
3930
+ "text": "HTMLDialogElement"
3931
+ }
3932
+ },
3933
+ {
3934
+ "name": "entering",
3935
+ "type": {
3936
+ "text": "boolean"
3937
+ }
3938
+ }
3939
+ ]
3940
+ },
3803
3941
  {
3804
3942
  "kind": "method",
3805
3943
  "name": "_focusInitialElement",
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Whether a section animates in. Components with their own motion
3
+ * vocabulary (Modal's direction values) declare their own `motion` values.
4
+ */
5
+ export declare const motionValues: {
6
+ readonly ON: "on";
7
+ readonly OFF: "off";
8
+ };
9
+ export declare const motionProps: import("lit").PropertyDeclaration<unknown, unknown>;
10
+ export type MotionType = (typeof motionValues)[keyof typeof motionValues];
@@ -156,20 +156,8 @@ class Base extends LitElement {
156
156
  }
157
157
  defineElement("dsgn-base", Base);
158
158
 
159
- // common/helpers/motion.ts
160
- var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
161
- var cachedMatchMedia;
162
- var cachedQuery = null;
163
- function prefersReducedMotion() {
164
- const mm = typeof matchMedia === "function" ? matchMedia : undefined;
165
- if (mm !== cachedMatchMedia) {
166
- cachedMatchMedia = mm;
167
- cachedQuery = mm ? mm(REDUCED_MOTION_QUERY) : null;
168
- }
169
- return cachedQuery?.matches ?? false;
170
- }
171
-
172
159
  // common/controllers/AuroraMeshController.ts
160
+ import { prefersReducedMotion } from "@ds-gn/motion";
173
161
  var X_MIN = -15;
174
162
  var X_MAX = 70;
175
163
  var Y_MIN = -20;
@@ -8,6 +8,7 @@ import '../../Icon/src/Icon.js';
8
8
  import { Base } from '../../../common/Base/Base.js';
9
9
  import { type SurfaceType } from '../../../common/props/surface/surface.props.js';
10
10
  import { type CSSResultGroup } from 'lit';
11
+ import { type MotionType } from '../../../common/props/motion/motion.props.js';
11
12
  import { type SectionSpaceType } from '../../Container/src/Container.props.js';
12
13
  import { type FeatureTabData } from './FeatureTabs.props.js';
13
14
  /** @slot header - Optional content above the tab interface. */
@@ -19,6 +20,11 @@ export declare class FeatureTabs extends Base {
19
20
  paddingBottom?: SectionSpaceType;
20
21
  private _inView;
21
22
  private _rotate;
23
+ private _motion;
24
+ /** The feature whose detail last faded in; the fade replays when it changes. */
25
+ private _shownIndex?;
26
+ /** Motion progress, decided by the first visibility report. */
27
+ private _motionState?;
22
28
  surface: SurfaceType;
23
29
  features: FeatureTabData[];
24
30
  duration: number;
@@ -34,9 +40,31 @@ export declare class FeatureTabs extends Base {
34
40
  resumeAriaLabel: string;
35
41
  /** Accessible label for the tab list. */
36
42
  tabsAriaLabel: string;
43
+ /**
44
+ * Animate the section in when it scrolls into view: the header's heading
45
+ * letter by letter, then its description, then the tabs as a whole. The
46
+ * eyebrow stays static. A section already in view when it loads, and
47
+ * reduced motion, stay static.
48
+ */
49
+ motion: MotionType;
37
50
  constructor();
38
51
  firstUpdated(): void;
52
+ /**
53
+ * Decide the motion from the first visibility report: a section already
54
+ * on screen stays static; one off screen holds its parts hidden (from
55
+ * script, so nothing is hidden when scripts do not run) and plays when it
56
+ * comes into view, so even a jump to it shows no unanimated frame.
57
+ */
58
+ private _updateMotion;
59
+ private _motionParts;
60
+ private _playMotion;
39
61
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
62
+ /**
63
+ * Fade the detail in whenever the active feature changes. `keyed` renders a
64
+ * fresh element per feature, so each change animates a new node. This runs
65
+ * after render and before paint, so the new detail never shows unfaded.
66
+ */
67
+ private _fadeInDetail;
40
68
  private _handleTabClick;
41
69
  private _handleTabKeyDown;
42
70
  private _handlePauseResume;
@@ -95,20 +95,8 @@ class Base extends LitElement {
95
95
  }
96
96
  defineElement("dsgn-base", Base);
97
97
 
98
- // common/helpers/motion.ts
99
- var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
100
- var cachedMatchMedia;
101
- var cachedQuery = null;
102
- function prefersReducedMotion() {
103
- const mm = typeof matchMedia === "function" ? matchMedia : undefined;
104
- if (mm !== cachedMatchMedia) {
105
- cachedMatchMedia = mm;
106
- cachedQuery = mm ? mm(REDUCED_MOTION_QUERY) : null;
107
- }
108
- return cachedQuery?.matches ?? false;
109
- }
110
-
111
98
  // common/controllers/AutoRotateController.ts
99
+ import { prefersReducedMotion } from "@ds-gn/motion";
112
100
  var TICK_MS = 32;
113
101
 
114
102
  class AutoRotateController {
@@ -229,6 +217,13 @@ var surfaceValues = {
229
217
  };
230
218
  var surfaceProps = enumCreator(Object.values(surfaceValues), "surface", String, { reflect: true });
231
219
 
220
+ // components/FeatureTabs/src/FeatureTabs.ts
221
+ import {
222
+ prefersReducedMotion as prefersReducedMotion2,
223
+ splitText
224
+ } from "@ds-gn/motion";
225
+ import { MotionController } from "@ds-gn/motion/lit";
226
+
232
227
  // ../../node_modules/.bun/lit-html@3.3.3/node_modules/lit-html/development/is-server.js
233
228
  var NODE_MODE = false;
234
229
  var isServer = NODE_MODE;
@@ -303,6 +298,15 @@ import {
303
298
  import { property } from "lit/decorators.js";
304
299
  import { keyed } from "lit/directives/keyed.js";
305
300
  import { styleMap } from "lit/directives/style-map.js";
301
+
302
+ // common/props/motion/motion.props.ts
303
+ var motionValues = {
304
+ ON: "on",
305
+ OFF: "off"
306
+ };
307
+ var motionProps = enumCreator(Object.values(motionValues), "motion", String, { reflect: true });
308
+
309
+ // components/FeatureTabs/src/FeatureTabs.ts
306
310
  import {
307
311
  containerSizeValues,
308
312
  paddingBottomProps,
@@ -338,8 +342,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
338
342
  --color-text-primary: inherit;
339
343
  --color-text-secondary: inherit;
340
344
  --color-text-tertiary: inherit;
341
- --animate-fade-in-a: fade-in-a 0.3s ease-in;
342
- --animate-fade-in-b: fade-in-b 0.3s ease-in;
343
345
  }
344
346
  }
345
347
  @layer utilities {
@@ -463,12 +465,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
463
465
  .shrink-0 {
464
466
  flex-shrink: 0;
465
467
  }
466
- .animate-fade-in-a {
467
- animation: var(--animate-fade-in-a);
468
- }
469
- .animate-fade-in-b {
470
- animation: var(--animate-fade-in-b);
471
- }
472
468
  .cursor-pointer {
473
469
  cursor: pointer;
474
470
  }
@@ -850,22 +846,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
850
846
  syntax: "*";
851
847
  inherits: false;
852
848
  }
853
- @keyframes fade-in-a {
854
- from {
855
- opacity: 0;
856
- }
857
- to {
858
- opacity: 1;
859
- }
860
- }
861
- @keyframes fade-in-b {
862
- from {
863
- opacity: 0;
864
- }
865
- to {
866
- opacity: 1;
867
- }
868
- }
869
849
  @layer properties {
870
850
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
871
851
  *, ::before, ::after, ::backdrop {
@@ -1019,7 +999,7 @@ var patternOverlayStyles = () => ({
1019
999
  maskImage: PATTERN_MASK,
1020
1000
  WebkitMaskImage: PATTERN_MASK
1021
1001
  });
1022
- var detailContentClassNames = (idx) => clsx_default("relative", "flex", "flex-col", "flex-1", idx % 2 === 0 ? "animate-fade-in-a" : "animate-fade-in-b");
1002
+ var detailContentClassNames = () => clsx_default("relative", "flex", "flex-col", "flex-1");
1023
1003
  var detailHeaderClassNames = () => clsx_default("flex", "items-center", "gap-3", "mb-6");
1024
1004
  var detailEyebrowClassNames = () => clsx_default("font-mono", "text-[11px]", "tracking-[0.06em]", "uppercase", "text-text-tertiary");
1025
1005
  var detailCounterClassNames = () => clsx_default("ml-auto", "font-mono", "text-[12px]", "text-text-tertiary");
@@ -1036,13 +1016,20 @@ var dotPipInnerClassNames = (active) => clsx_default("block", "rounded-full", "h
1036
1016
  var playPauseClassNames = () => clsx_default("appearance-none", "border-0", "outline-none", "bg-transparent", "cursor-pointer", "text-accent", "py-[10px]", "px-[8px]", "flex", "items-center", "justify-center", "rounded-full", "transition-colors", "duration-150");
1037
1017
 
1038
1018
  // components/FeatureTabs/src/FeatureTabs.ts
1019
+ var holdHidden = (el) => el.style.setProperty("opacity", "0");
1020
+ var releaseHidden = (el) => el.style.removeProperty("opacity");
1021
+ var HEADING_STAGGER = 20;
1022
+ var HEADING_MAX_SPREAD = 500;
1023
+ var HEADING_RISE = 4;
1024
+ var MOTION_OVERLAP = 160;
1039
1025
  var FEATURE_TAB_ICONS = {
1040
1026
  terminal: svg`<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>`,
1041
1027
  brain: svg`<path d="M9 4.5a2.5 2.5 0 0 0-2.5 2.5v.5a2.5 2.5 0 0 0-2 4 2.5 2.5 0 0 0 2 4v.5A2.5 2.5 0 0 0 9 18.5a2.5 2.5 0 0 0 3-2.5V6a2.5 2.5 0 0 0-3-1.5z"/><path d="M15 4.5A2.5 2.5 0 0 1 17.5 7v.5a2.5 2.5 0 0 1 2 4 2.5 2.5 0 0 1-2 4v.5A2.5 2.5 0 0 1 15 18.5a2.5 2.5 0 0 1-3-2.5V6a2.5 2.5 0 0 1 3-1.5z"/>`,
1042
1028
  cpu: svg`<rect x="5" y="5" width="14" height="14" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="2" x2="9" y2="5"/><line x1="15" y1="2" x2="15" y2="5"/><line x1="9" y1="19" x2="9" y2="22"/><line x1="15" y1="19" x2="15" y2="22"/><line x1="2" y1="9" x2="5" y2="9"/><line x1="2" y1="15" x2="5" y2="15"/><line x1="19" y1="9" x2="22" y2="9"/><line x1="19" y1="15" x2="22" y2="15"/>`,
1043
1029
  palette: svg`<circle cx="12" cy="12" r="9"/><circle cx="7.5" cy="10.5" r="1" fill="currentColor"/><circle cx="12" cy="7.5" r="1" fill="currentColor"/><circle cx="16.5" cy="10.5" r="1" fill="currentColor"/><path d="M12 21a3 3 0 0 1-3-3c0-1.5 1-2 1-3a2 2 0 0 0-2-2"/>`,
1044
1030
  plug: svg`<path d="M9 2v6"/><path d="M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-5 5 5 5 0 0 1-5-5z"/><path d="M12 16v6"/>`,
1045
- shield: svg`<path d="M12 3l8 3v5c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/><path d="M9 12l2 2 4-4"/>`
1031
+ shield: svg`<path d="M12 3l8 3v5c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6z"/><path d="M9 12l2 2 4-4"/>`,
1032
+ zap: svg`<path d="M13 2 4 14h7l-1 8 9-12h-7z"/>`
1046
1033
  };
1047
1034
  var nextTabIndex = (key, index, lastIndex) => {
1048
1035
  switch (key) {
@@ -1069,6 +1056,7 @@ class FeatureTabs extends Base {
1069
1056
  config: { threshold: 0 },
1070
1057
  callback: (entries) => entries[entries.length - 1]?.isIntersecting ?? false
1071
1058
  });
1059
+ this._motion = new MotionController(this);
1072
1060
  this.surface = surfaceValues.DARK;
1073
1061
  this.features = DEFAULT_FEATURES;
1074
1062
  this.duration = DEFAULT_DURATION;
@@ -1078,17 +1066,82 @@ class FeatureTabs extends Base {
1078
1066
  this.pauseAriaLabel = DEFAULT_PAUSE_ARIA_LABEL;
1079
1067
  this.resumeAriaLabel = DEFAULT_RESUME_ARIA_LABEL;
1080
1068
  this.tabsAriaLabel = DEFAULT_TABS_ARIA_LABEL;
1069
+ this.motion = motionValues.OFF;
1081
1070
  this._rotate = new AutoRotateController(this, DEFAULT_DURATION, () => this._inView.value === true);
1082
1071
  }
1083
1072
  firstUpdated() {
1084
1073
  this._rotate.resume(this.features.length);
1085
1074
  }
1075
+ _updateMotion() {
1076
+ const inView = this._inView.value;
1077
+ if (inView === undefined)
1078
+ return;
1079
+ if (this._motionState === undefined) {
1080
+ const play = this.motion === motionValues.ON && !inView && !prefersReducedMotion2();
1081
+ this._motionState = play ? "held" : "static";
1082
+ if (play)
1083
+ this._motionParts().forEach(holdHidden);
1084
+ }
1085
+ if (this._motionState === "held" && inView) {
1086
+ this._motionState = "played";
1087
+ this._playMotion();
1088
+ }
1089
+ }
1090
+ _motionParts() {
1091
+ const header = Array.from(this.children).filter((el) => el.slot === "header");
1092
+ const find = (slot) => header.map((el) => el.querySelector(`[slot="${slot}"]`)).find((el) => el !== null);
1093
+ return [
1094
+ find("heading"),
1095
+ find("description"),
1096
+ ...this.renderRoot.querySelectorAll("[data-motion-body]")
1097
+ ].filter((el) => el !== undefined);
1098
+ }
1099
+ _playMotion() {
1100
+ const parts = this._motionParts();
1101
+ parts.forEach(releaseHidden);
1102
+ const heading = parts.find((el) => el.slot === "heading");
1103
+ const description = parts.find((el) => el.slot === "description");
1104
+ const split = heading ? splitText(heading) : undefined;
1105
+ const steps = [
1106
+ ...split ? [
1107
+ {
1108
+ targets: split.chars,
1109
+ preset: "fade",
1110
+ y: HEADING_RISE,
1111
+ stagger: HEADING_STAGGER,
1112
+ maxDuration: HEADING_MAX_SPREAD
1113
+ }
1114
+ ] : [],
1115
+ ...description ? [
1116
+ {
1117
+ el: description,
1118
+ preset: "fade",
1119
+ overlap: MOTION_OVERLAP
1120
+ }
1121
+ ] : [],
1122
+ {
1123
+ targets: parts.filter((el) => el.hasAttribute("data-motion-body")),
1124
+ preset: "fade",
1125
+ overlap: MOTION_OVERLAP
1126
+ }
1127
+ ];
1128
+ this._motion.timeline(steps).finished.then(() => split?.revert());
1129
+ }
1086
1130
  updated(changedProperties) {
1087
1131
  super.updated(changedProperties);
1088
1132
  this.updateTheme(changedProperties, this.surface);
1089
1133
  if (changedProperties.has("duration")) {
1090
1134
  this._rotate.setDuration(this.duration);
1091
1135
  }
1136
+ this._fadeInDetail();
1137
+ this._updateMotion();
1138
+ }
1139
+ _fadeInDetail() {
1140
+ const index = this._rotate.activeIndex;
1141
+ if (index === this._shownIndex)
1142
+ return;
1143
+ this._shownIndex = index;
1144
+ this.renderRoot.querySelectorAll("[data-detail-content]").forEach((detail) => void this._motion.enter(detail, "fade"));
1092
1145
  }
1093
1146
  _handleTabClick(index) {
1094
1147
  this._rotate.goTo(index);
@@ -1148,7 +1201,7 @@ class FeatureTabs extends Base {
1148
1201
  <div aria-hidden="true" style=${styleMap(patternOverlayStyles())}></div>
1149
1202
 
1150
1203
  ${keyed(idx, html2`
1151
- <div class="${detailContentClassNames(idx)}">
1204
+ <div class="${detailContentClassNames()}" data-detail-content>
1152
1205
  <div class="${detailHeaderClassNames()}">
1153
1206
  <span class="${iconBadgeClassNames(true, true)}">
1154
1207
  ${this._renderIcon(feature.icon, "lg")}
@@ -1286,12 +1339,12 @@ class FeatureTabs extends Base {
1286
1339
  </div>
1287
1340
 
1288
1341
  <!-- Mobile: detail panel only, no grid -->
1289
- <div class="lg:hidden">
1342
+ <div class="lg:hidden" data-motion-body>
1290
1343
  ${this._renderDetailPanel(idx, activeFeature, progress, total, paused, features)}
1291
1344
  </div>
1292
1345
 
1293
1346
  <!-- Desktop: full grid with tabs + detail -->
1294
- <div class="hidden lg:block">
1347
+ <div class="hidden lg:block" data-motion-body>
1295
1348
  <dsgn-grid cols="7-5" gap="xl" ?reverse=${true}>
1296
1349
  ${this._renderTabList(features, idx, progress, paused, "col-2")}
1297
1350
  ${this._renderDetailPanel(idx, activeFeature, progress, total, paused, features, "col-1")}
@@ -1335,6 +1388,9 @@ __legacyDecorateClassTS([
1335
1388
  __legacyDecorateClassTS([
1336
1389
  property({ type: String, attribute: "tabs-aria-label" })
1337
1390
  ], FeatureTabs.prototype, "tabsAriaLabel", undefined);
1391
+ __legacyDecorateClassTS([
1392
+ property({ ...motionProps })
1393
+ ], FeatureTabs.prototype, "motion", undefined);
1338
1394
  defineElement("dsgn-feature-tabs", FeatureTabs);
1339
1395
  export {
1340
1396
  FeatureTabs
@@ -1,4 +1,4 @@
1
- export type FeatureTabIcon = 'terminal' | 'brain' | 'cpu' | 'palette' | 'plug' | 'shield';
1
+ export type FeatureTabIcon = 'terminal' | 'brain' | 'cpu' | 'palette' | 'plug' | 'shield' | 'zap';
2
2
  export interface FeatureTabData {
3
3
  id: string;
4
4
  eyebrow: string;
@@ -18,7 +18,7 @@ export declare const controlBtnClassNames: () => string;
18
18
  export declare const controlDotClassNames: (rotating: boolean) => string;
19
19
  export declare const detailClassNames: () => string;
20
20
  export declare const patternOverlayStyles: () => Record<string, string>;
21
- export declare const detailContentClassNames: (idx: number) => string;
21
+ export declare const detailContentClassNames: () => string;
22
22
  export declare const detailHeaderClassNames: () => string;
23
23
  export declare const detailEyebrowClassNames: () => string;
24
24
  export declare const detailCounterClassNames: () => string;
@@ -157,20 +157,8 @@ class Base extends LitElement {
157
157
  }
158
158
  defineElement("dsgn-base", Base);
159
159
 
160
- // common/helpers/motion.ts
161
- var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
162
- var cachedMatchMedia;
163
- var cachedQuery = null;
164
- function prefersReducedMotion() {
165
- const mm = typeof matchMedia === "function" ? matchMedia : undefined;
166
- if (mm !== cachedMatchMedia) {
167
- cachedMatchMedia = mm;
168
- cachedQuery = mm ? mm(REDUCED_MOTION_QUERY) : null;
169
- }
170
- return cachedQuery?.matches ?? false;
171
- }
172
-
173
160
  // common/controllers/WordCycleController.ts
161
+ import { prefersReducedMotion } from "@ds-gn/motion";
174
162
  var DEFAULT_INTERVAL = 1000;
175
163
 
176
164
  class WordCycleController {
@@ -5,6 +5,7 @@ import '../../Grid/src/Grid.js';
5
5
  import '../../HeadingBlock/src/HeadingBlock.js';
6
6
  import { type CSSResultGroup } from 'lit';
7
7
  import { Base } from '../../../common/Base/Base.js';
8
+ import { type MotionType } from '../../../common/props/motion/motion.props.js';
8
9
  import { type SurfaceType } from '../../../common/props/surface/surface.props.js';
9
10
  import { type SectionSpaceType } from '../../Container/src/Container.props.js';
10
11
  import { type HeroContentAlignType, type HeroLayoutType, type HeroMinHeightType } from './Hero.props.js';
@@ -37,6 +38,16 @@ export declare class Hero extends Base {
37
38
  paddingTop?: SectionSpaceType;
38
39
  /** Space below the section. Unset follows the inherited section space. */
39
40
  paddingBottom?: SectionSpaceType;
41
+ /**
42
+ * Animate the content's eyebrow in letter by letter once the hero loads,
43
+ * each letter fading in with a small rise.
44
+ * Everything else stays static, so the hero's largest text paints at once.
45
+ * Under reduced motion the eyebrow is static too.
46
+ */
47
+ motion: MotionType;
48
+ private _motion;
49
+ firstUpdated(): void;
50
+ private _eyebrows;
40
51
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
41
52
  render(): import("lit").TemplateResult<1>;
42
53
  }
@@ -14,6 +14,8 @@ import"../../Container/src/Container.js";
14
14
  import"../../FormBlock/src/FormBlock.js";
15
15
  import"../../Grid/src/Grid.js";
16
16
  import"../../HeadingBlock/src/HeadingBlock.js";
17
+ import { prefersReducedMotion, splitText } from "@ds-gn/motion";
18
+ import { MotionController } from "@ds-gn/motion/lit";
17
19
  import { html as html2 } from "lit";
18
20
  import { property } from "lit/decorators.js";
19
21
 
@@ -118,6 +120,13 @@ var enumCreator = (acceptableValues, propName, type, other = {}) => {
118
120
  };
119
121
  };
120
122
 
123
+ // common/props/motion/motion.props.ts
124
+ var motionValues = {
125
+ ON: "on",
126
+ OFF: "off"
127
+ };
128
+ var motionProps = enumCreator(Object.values(motionValues), "motion", String, { reflect: true });
129
+
121
130
  // common/props/surface/surface.props.ts
122
131
  var surfaceValues = {
123
132
  LIGHT: "light",
@@ -189,6 +198,9 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
189
198
  .grid {
190
199
  display: grid;
191
200
  }
201
+ .hidden {
202
+ display: none;
203
+ }
192
204
  .h-64 {
193
205
  height: calc(var(--spacing) * 64);
194
206
  }
@@ -412,6 +424,11 @@ var centeredFormClassNames = (contentAlign) => {
412
424
  };
413
425
 
414
426
  // components/Hero/src/Hero.ts
427
+ var EYEBROW_DELAY = 500;
428
+ var EYEBROW_STAGGER = 30;
429
+ var EYEBROW_MAX_SPREAD = 600;
430
+ var EYEBROW_RISE = 4;
431
+
415
432
  class Hero extends Base {
416
433
  constructor() {
417
434
  super(...arguments);
@@ -419,8 +436,27 @@ class Hero extends Base {
419
436
  this.layout = heroLayoutValues.ASSET_RIGHT;
420
437
  this.minHeight = heroMinHeightValues.MD;
421
438
  this.contentAlign = heroContentAlignValues.START;
439
+ this.motion = motionValues.OFF;
440
+ this._motion = new MotionController(this);
422
441
  }
423
442
  static styles = [Base.styles, Hero_default];
443
+ firstUpdated() {
444
+ if (this.motion !== motionValues.ON || prefersReducedMotion())
445
+ return;
446
+ this._eyebrows().forEach((eyebrow) => {
447
+ const split = splitText(eyebrow);
448
+ this._motion.sequence(split.chars, {
449
+ preset: "fade",
450
+ y: EYEBROW_RISE,
451
+ delay: EYEBROW_DELAY,
452
+ stagger: EYEBROW_STAGGER,
453
+ maxDuration: EYEBROW_MAX_SPREAD
454
+ }).finished.then(split.revert);
455
+ });
456
+ }
457
+ _eyebrows() {
458
+ return Array.from(this.children).filter((el) => el.slot === "content").flatMap((el) => Array.from(el.querySelectorAll('[slot="eyebrow"]')));
459
+ }
424
460
  updated(changedProperties) {
425
461
  super.updated(changedProperties);
426
462
  this.updateTheme(changedProperties, this.surface);
@@ -526,6 +562,9 @@ __legacyDecorateClassTS([
526
562
  __legacyDecorateClassTS([
527
563
  property({ ...paddingBottomProps })
528
564
  ], Hero.prototype, "paddingBottom", undefined);
565
+ __legacyDecorateClassTS([
566
+ property({ ...motionProps })
567
+ ], Hero.prototype, "motion", undefined);
529
568
  defineElement("dsgn-hero", Hero);
530
569
  export {
531
570
  Hero
@@ -72,8 +72,10 @@ export declare class Modal extends Base {
72
72
  * keyboard users are stuck.
73
73
  */
74
74
  noEscapeDismiss: boolean;
75
- /** Drives `data-state`, which the stylesheet keys the exit animation on. */
75
+ private readonly _motion;
76
76
  private _closing;
77
+ /** Invalidates an exit when reopened, disconnected, or closed externally. */
78
+ private _closeRun;
77
79
  private _hasHeading;
78
80
  private _hasDescription;
79
81
  private _hasFooter;
@@ -118,6 +120,7 @@ export declare class Modal extends Base {
118
120
  * on the animations' own `finished` promises.
119
121
  */
120
122
  private _runCloseSequence;
123
+ private _animateDialog;
121
124
  /**
122
125
  * Deliberate initial focus: an author-marked `[autofocus]` (light DOM first,
123
126
  * since that is where the modal's content lives), otherwise the dialog itself.
@@ -11,6 +11,8 @@ var __legacyDecorateClassTS = function(decorators, target, key, desc) {
11
11
 
12
12
  // components/Modal/src/Modal.ts
13
13
  import"../../Button/src/Button.js";
14
+ import { cancel } from "@ds-gn/motion";
15
+ import { MotionController } from "@ds-gn/motion/lit";
14
16
  import { html as html2, nothing } from "lit";
15
17
  import { property, query, state } from "lit/decorators.js";
16
18
 
@@ -146,9 +148,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
146
148
  --font-weight-semibold: 600;
147
149
  --default-transition-duration: 150ms;
148
150
  --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
149
- --duration-moderate: var(--dsgn-src-duration-moderate, 240ms);
150
- --duration-quick: var(--dsgn-src-duration-quick, 180ms);
151
- --ease-emphasized: var(--dsgn-src-ease-emphasized, cubic-bezier(0.16, 1, 0.3, 1));
152
151
  --radius-surface: var(--dsgn-src-radius-surface, 1rem);
153
152
  --color-accent: inherit;
154
153
  --color-focus: inherit;
@@ -156,29 +155,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
156
155
  --color-text-primary: inherit;
157
156
  --color-text-secondary: inherit;
158
157
  --shadow-card: inherit;
159
- --animate-fade-in: fade-in var(--duration-moderate) var(--ease-emphasized)
160
- both;
161
- --animate-fade-out: fade-out var(--duration-quick) var(--ease-emphasized) both;
162
- --animate-scale-in: scale-in var(--duration-moderate) var(--ease-emphasized)
163
- both;
164
- --animate-scale-out: scale-out var(--duration-quick) var(--ease-emphasized)
165
- both;
166
- --animate-slide-in-left: slide-in-left var(--duration-moderate)
167
- var(--ease-emphasized) both;
168
- --animate-slide-out-left: slide-out-left var(--duration-quick)
169
- var(--ease-emphasized) both;
170
- --animate-slide-in-right: slide-in-right var(--duration-moderate)
171
- var(--ease-emphasized) both;
172
- --animate-slide-out-right: slide-out-right var(--duration-quick)
173
- var(--ease-emphasized) both;
174
- --animate-slide-in-top: slide-in-top var(--duration-moderate)
175
- var(--ease-emphasized) both;
176
- --animate-slide-out-top: slide-out-top var(--duration-quick)
177
- var(--ease-emphasized) both;
178
- --animate-slide-in-bottom: slide-in-bottom var(--duration-moderate)
179
- var(--ease-emphasized) both;
180
- --animate-slide-out-bottom: slide-out-bottom var(--duration-quick)
181
- var(--ease-emphasized) both;
182
158
  }
183
159
  }
184
160
  @layer utilities {
@@ -640,122 +616,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
640
616
  --dsgn-modal-size: 56rem;
641
617
  }
642
618
  }
643
- .data-\\[state\\=closed\\]\\:backdrop\\:animate-fade-out {
644
- &[data-state="closed"] {
645
- &::backdrop {
646
- animation: var(--animate-fade-out);
647
- }
648
- }
649
- }
650
- .data-\\[motion\\=bottom\\]\\:data-\\[state\\=closed\\]\\:animate-slide-out-bottom {
651
- &[data-motion="bottom"] {
652
- &[data-state="closed"] {
653
- animation: var(--animate-slide-out-bottom);
654
- }
655
- }
656
- }
657
- .data-\\[motion\\=center\\]\\:data-\\[state\\=closed\\]\\:animate-scale-out {
658
- &[data-motion="center"] {
659
- &[data-state="closed"] {
660
- animation: var(--animate-scale-out);
661
- }
662
- }
663
- }
664
- .data-\\[motion\\=left\\]\\:data-\\[state\\=closed\\]\\:animate-slide-out-left {
665
- &[data-motion="left"] {
666
- &[data-state="closed"] {
667
- animation: var(--animate-slide-out-left);
668
- }
669
- }
670
- }
671
- .data-\\[motion\\=none\\]\\:data-\\[state\\=closed\\]\\:animate-fade-out {
672
- &[data-motion="none"] {
673
- &[data-state="closed"] {
674
- animation: var(--animate-fade-out);
675
- }
676
- }
677
- }
678
- .data-\\[motion\\=right\\]\\:data-\\[state\\=closed\\]\\:animate-slide-out-right {
679
- &[data-motion="right"] {
680
- &[data-state="closed"] {
681
- animation: var(--animate-slide-out-right);
682
- }
683
- }
684
- }
685
- .data-\\[motion\\=top\\]\\:data-\\[state\\=closed\\]\\:animate-slide-out-top {
686
- &[data-motion="top"] {
687
- &[data-state="closed"] {
688
- animation: var(--animate-slide-out-top);
689
- }
690
- }
691
- }
692
- .data-\\[state\\=open\\]\\:backdrop\\:animate-fade-in {
693
- &[data-state="open"] {
694
- &::backdrop {
695
- animation: var(--animate-fade-in);
696
- }
697
- }
698
- }
699
- .data-\\[motion\\=bottom\\]\\:data-\\[state\\=open\\]\\:animate-slide-in-bottom {
700
- &[data-motion="bottom"] {
701
- &[data-state="open"] {
702
- animation: var(--animate-slide-in-bottom);
703
- }
704
- }
705
- }
706
- .data-\\[motion\\=center\\]\\:data-\\[state\\=open\\]\\:animate-scale-in {
707
- &[data-motion="center"] {
708
- &[data-state="open"] {
709
- animation: var(--animate-scale-in);
710
- }
711
- }
712
- }
713
- .data-\\[motion\\=left\\]\\:data-\\[state\\=open\\]\\:animate-slide-in-left {
714
- &[data-motion="left"] {
715
- &[data-state="open"] {
716
- animation: var(--animate-slide-in-left);
717
- }
718
- }
719
- }
720
- .data-\\[motion\\=none\\]\\:data-\\[state\\=open\\]\\:animate-fade-in {
721
- &[data-motion="none"] {
722
- &[data-state="open"] {
723
- animation: var(--animate-fade-in);
724
- }
725
- }
726
- }
727
- .data-\\[motion\\=right\\]\\:data-\\[state\\=open\\]\\:animate-slide-in-right {
728
- &[data-motion="right"] {
729
- &[data-state="open"] {
730
- animation: var(--animate-slide-in-right);
731
- }
732
- }
733
- }
734
- .data-\\[motion\\=top\\]\\:data-\\[state\\=open\\]\\:animate-slide-in-top {
735
- &[data-motion="top"] {
736
- &[data-state="open"] {
737
- animation: var(--animate-slide-in-top);
738
- }
739
- }
740
- }
741
- .motion-reduce\\:data-\\[motion\\]\\:data-\\[state\\=closed\\]\\:animate-fade-out {
742
- @media (prefers-reduced-motion: reduce) {
743
- &[data-motion] {
744
- &[data-state="closed"] {
745
- animation: var(--animate-fade-out);
746
- }
747
- }
748
- }
749
- }
750
- .motion-reduce\\:data-\\[motion\\]\\:data-\\[state\\=open\\]\\:animate-fade-in {
751
- @media (prefers-reduced-motion: reduce) {
752
- &[data-motion] {
753
- &[data-state="open"] {
754
- animation: var(--animate-fade-in);
755
- }
756
- }
757
- }
758
- }
759
619
  }
760
620
  @property --dsgn-border-sweep-angle {
761
621
  syntax: '<angle>';
@@ -877,106 +737,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
877
737
  inherits: false;
878
738
  initial-value: solid;
879
739
  }
880
- @keyframes fade-in {
881
- from {
882
- opacity: 0;
883
- }
884
- to {
885
- opacity: 1;
886
- }
887
- }
888
- @keyframes fade-out {
889
- from {
890
- opacity: 1;
891
- }
892
- to {
893
- opacity: 0;
894
- }
895
- }
896
- @keyframes scale-in {
897
- from {
898
- opacity: 0;
899
- transform: scale(0.96);
900
- }
901
- to {
902
- opacity: 1;
903
- transform: none;
904
- }
905
- }
906
- @keyframes scale-out {
907
- from {
908
- opacity: 1;
909
- transform: none;
910
- }
911
- to {
912
- opacity: 0;
913
- transform: scale(0.96);
914
- }
915
- }
916
- @keyframes slide-in-left {
917
- from {
918
- transform: translateX(-100%);
919
- }
920
- to {
921
- transform: none;
922
- }
923
- }
924
- @keyframes slide-out-left {
925
- from {
926
- transform: none;
927
- }
928
- to {
929
- transform: translateX(-100%);
930
- }
931
- }
932
- @keyframes slide-in-right {
933
- from {
934
- transform: translateX(100%);
935
- }
936
- to {
937
- transform: none;
938
- }
939
- }
940
- @keyframes slide-out-right {
941
- from {
942
- transform: none;
943
- }
944
- to {
945
- transform: translateX(100%);
946
- }
947
- }
948
- @keyframes slide-in-top {
949
- from {
950
- transform: translateY(-100%);
951
- }
952
- to {
953
- transform: none;
954
- }
955
- }
956
- @keyframes slide-out-top {
957
- from {
958
- transform: none;
959
- }
960
- to {
961
- transform: translateY(-100%);
962
- }
963
- }
964
- @keyframes slide-in-bottom {
965
- from {
966
- transform: translateY(100%);
967
- }
968
- to {
969
- transform: none;
970
- }
971
- }
972
- @keyframes slide-out-bottom {
973
- from {
974
- transform: none;
975
- }
976
- to {
977
- transform: translateY(100%);
978
- }
979
- }
980
740
  @layer properties {
981
741
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
982
742
  *, ::before, ::after, ::backdrop {
@@ -1031,6 +791,14 @@ var modalMotionValues = {
1031
791
  TOP: "top",
1032
792
  BOTTOM: "bottom"
1033
793
  };
794
+ var modalMotionPresets = {
795
+ center: "scaleIn",
796
+ left: "slideInLeft",
797
+ right: "slideInRight",
798
+ top: "slideInTop",
799
+ bottom: "slideInBottom",
800
+ none: "fade"
801
+ };
1034
802
  var modalMotionProps = enumCreator(Object.values(modalMotionValues), "motion", String, { reflect: true });
1035
803
  var modalSizeValues = {
1036
804
  SM: "sm",
@@ -1077,7 +845,7 @@ var hostStyles = css3`
1077
845
  display: contents;
1078
846
  }
1079
847
  `;
1080
- var dialogClasses = () => clsx_default("box-border", "p-0", "border-0", "overflow-hidden", "flex-col", "open:flex", "text-text-primary", "bg-surface-elevated", "rounded-[var(--radius-surface)]", "shadow-[var(--shadow-card),0_0_18px_color-mix(in_oklab,var(--color-accent)_14.625%,transparent)]", "focus-visible:outline-2", "focus-visible:outline-focus", "focus-visible:outline-offset-2", "[--dsgn-modal-size:32rem]", "[--dsgn-modal-gutter:1rem]", "data-[size=sm]:[--dsgn-modal-size:24rem]", "data-[size=md]:[--dsgn-modal-size:32rem]", "data-[size=lg]:[--dsgn-modal-size:42rem]", "data-[size=xl]:[--dsgn-modal-size:56rem]", "data-[placement=center]:m-auto", "data-[placement=center]:w-[min(var(--dsgn-modal-size),calc(100vw-2*var(--dsgn-modal-gutter)))]", "data-[placement=center]:max-h-[calc(100dvh-2*var(--dsgn-modal-gutter))]", "data-[placement=left]:m-0", "data-[placement=left]:top-0", "data-[placement=left]:bottom-0", "data-[placement=left]:h-dvh", "data-[placement=left]:max-h-dvh", "data-[placement=left]:w-[min(var(--dsgn-modal-size),calc(100vw-var(--dsgn-modal-gutter)))]", "data-[placement=left]:left-0", "data-[placement=left]:right-auto", "data-[placement=left]:rounded-ss-none", "data-[placement=left]:rounded-es-none", "data-[placement=right]:m-0", "data-[placement=right]:top-0", "data-[placement=right]:bottom-0", "data-[placement=right]:h-dvh", "data-[placement=right]:max-h-dvh", "data-[placement=right]:w-[min(var(--dsgn-modal-size),calc(100vw-var(--dsgn-modal-gutter)))]", "data-[placement=right]:left-auto", "data-[placement=right]:right-0", "data-[placement=right]:rounded-se-none", "data-[placement=right]:rounded-ee-none", "data-[placement=top]:m-0", "data-[placement=top]:left-0", "data-[placement=top]:right-0", "data-[placement=top]:w-screen", "data-[placement=top]:max-w-screen", "data-[placement=top]:max-h-[min(var(--dsgn-modal-size),calc(100dvh-var(--dsgn-modal-gutter)))]", "data-[placement=top]:top-0", "data-[placement=top]:bottom-auto", "data-[placement=top]:rounded-ss-none", "data-[placement=top]:rounded-se-none", "data-[placement=bottom]:m-0", "data-[placement=bottom]:left-0", "data-[placement=bottom]:right-0", "data-[placement=bottom]:w-screen", "data-[placement=bottom]:max-w-screen", "data-[placement=bottom]:max-h-[min(var(--dsgn-modal-size),calc(100dvh-var(--dsgn-modal-gutter)))]", "data-[placement=bottom]:top-auto", "data-[placement=bottom]:bottom-0", "data-[placement=bottom]:rounded-es-none", "data-[placement=bottom]:rounded-ee-none", "data-[size=full]:inset-0", "data-[size=full]:m-0", "data-[size=full]:w-screen", "data-[size=full]:max-w-screen", "data-[size=full]:h-dvh", "data-[size=full]:max-h-dvh", "data-[size=full]:rounded-none", "backdrop:bg-[rgb(2_6_23/0.6)]", "backdrop:backdrop-blur-[12px]", "data-[state=open]:backdrop:animate-fade-in", "data-[state=closed]:backdrop:animate-fade-out", "data-[motion=center]:data-[state=open]:animate-scale-in", "data-[motion=center]:data-[state=closed]:animate-scale-out", "data-[motion=left]:data-[state=open]:animate-slide-in-left", "data-[motion=left]:data-[state=closed]:animate-slide-out-left", "data-[motion=right]:data-[state=open]:animate-slide-in-right", "data-[motion=right]:data-[state=closed]:animate-slide-out-right", "data-[motion=top]:data-[state=open]:animate-slide-in-top", "data-[motion=top]:data-[state=closed]:animate-slide-out-top", "data-[motion=bottom]:data-[state=open]:animate-slide-in-bottom", "data-[motion=bottom]:data-[state=closed]:animate-slide-out-bottom", "data-[motion=none]:data-[state=open]:animate-fade-in", "data-[motion=none]:data-[state=closed]:animate-fade-out", "motion-reduce:data-[motion]:data-[state=open]:animate-fade-in", "motion-reduce:data-[motion]:data-[state=closed]:animate-fade-out");
848
+ var dialogClasses = () => clsx_default("box-border", "p-0", "border-0", "overflow-hidden", "flex-col", "open:flex", "text-text-primary", "bg-surface-elevated", "rounded-[var(--radius-surface)]", "shadow-[var(--shadow-card),0_0_18px_color-mix(in_oklab,var(--color-accent)_14.625%,transparent)]", "focus-visible:outline-2", "focus-visible:outline-focus", "focus-visible:outline-offset-2", "[--dsgn-modal-size:32rem]", "[--dsgn-modal-gutter:1rem]", "data-[size=sm]:[--dsgn-modal-size:24rem]", "data-[size=md]:[--dsgn-modal-size:32rem]", "data-[size=lg]:[--dsgn-modal-size:42rem]", "data-[size=xl]:[--dsgn-modal-size:56rem]", "data-[placement=center]:m-auto", "data-[placement=center]:w-[min(var(--dsgn-modal-size),calc(100vw-2*var(--dsgn-modal-gutter)))]", "data-[placement=center]:max-h-[calc(100dvh-2*var(--dsgn-modal-gutter))]", "data-[placement=left]:m-0", "data-[placement=left]:top-0", "data-[placement=left]:bottom-0", "data-[placement=left]:h-dvh", "data-[placement=left]:max-h-dvh", "data-[placement=left]:w-[min(var(--dsgn-modal-size),calc(100vw-var(--dsgn-modal-gutter)))]", "data-[placement=left]:left-0", "data-[placement=left]:right-auto", "data-[placement=left]:rounded-ss-none", "data-[placement=left]:rounded-es-none", "data-[placement=right]:m-0", "data-[placement=right]:top-0", "data-[placement=right]:bottom-0", "data-[placement=right]:h-dvh", "data-[placement=right]:max-h-dvh", "data-[placement=right]:w-[min(var(--dsgn-modal-size),calc(100vw-var(--dsgn-modal-gutter)))]", "data-[placement=right]:left-auto", "data-[placement=right]:right-0", "data-[placement=right]:rounded-se-none", "data-[placement=right]:rounded-ee-none", "data-[placement=top]:m-0", "data-[placement=top]:left-0", "data-[placement=top]:right-0", "data-[placement=top]:w-screen", "data-[placement=top]:max-w-screen", "data-[placement=top]:max-h-[min(var(--dsgn-modal-size),calc(100dvh-var(--dsgn-modal-gutter)))]", "data-[placement=top]:top-0", "data-[placement=top]:bottom-auto", "data-[placement=top]:rounded-ss-none", "data-[placement=top]:rounded-se-none", "data-[placement=bottom]:m-0", "data-[placement=bottom]:left-0", "data-[placement=bottom]:right-0", "data-[placement=bottom]:w-screen", "data-[placement=bottom]:max-w-screen", "data-[placement=bottom]:max-h-[min(var(--dsgn-modal-size),calc(100dvh-var(--dsgn-modal-gutter)))]", "data-[placement=bottom]:top-auto", "data-[placement=bottom]:bottom-0", "data-[placement=bottom]:rounded-es-none", "data-[placement=bottom]:rounded-ee-none", "data-[size=full]:inset-0", "data-[size=full]:m-0", "data-[size=full]:w-screen", "data-[size=full]:max-w-screen", "data-[size=full]:h-dvh", "data-[size=full]:max-h-dvh", "data-[size=full]:rounded-none", "backdrop:bg-[rgb(2_6_23/0.6)]", "backdrop:backdrop-blur-[12px]");
1081
849
  var panelClasses = () => clsx_default("flex", "flex-col", "flex-auto", "min-h-0", "font-sans");
1082
850
  var headerClasses = (visible) => clsx_default("shrink-0", "items-start", "justify-between", "gap-4", "px-6", "pt-6", "pb-2", visible ? "flex" : "hidden");
1083
851
  var headingClasses = () => clsx_default("min-w-0", "text-lg", "font-semibold", "text-text-primary");
@@ -1118,17 +886,6 @@ var unlockBodyScroll = () => {
1118
886
  document.body.style.removeProperty("overflow");
1119
887
  }
1120
888
  };
1121
- var whenAnimationsSettle = async (dialog) => {
1122
- if (typeof dialog.getAnimations !== "function")
1123
- return;
1124
- const animations = dialog.getAnimations({ subtree: true }).filter((animation) => {
1125
- const effect = animation.effect;
1126
- return effect?.target === dialog;
1127
- });
1128
- if (animations.length === 0)
1129
- return;
1130
- await Promise.allSettled(animations.map((animation) => animation.finished));
1131
- };
1132
889
  var isInTopLayer = (dialog) => {
1133
890
  try {
1134
891
  return dialog.matches(":modal");
@@ -1150,7 +907,9 @@ class Modal extends Base {
1150
907
  this.noCloseControl = false;
1151
908
  this.noBackdropDismiss = false;
1152
909
  this.noEscapeDismiss = false;
910
+ this._motion = new MotionController(this);
1153
911
  this._closing = false;
912
+ this._closeRun = 0;
1154
913
  this._hasHeading = false;
1155
914
  this._hasDescription = false;
1156
915
  this._hasFooter = false;
@@ -1191,11 +950,13 @@ class Modal extends Base {
1191
950
  }
1192
951
  connectedCallback() {
1193
952
  super.connectedCallback();
1194
- if (this.open) {
953
+ if (this.open || this._dialog?.open) {
1195
954
  this.updateComplete.then(() => this._syncDialogState());
1196
955
  }
1197
956
  }
1198
957
  disconnectedCallback() {
958
+ this._closeRun += 1;
959
+ this._closing = false;
1199
960
  super.disconnectedCallback();
1200
961
  this._releaseScrollLock();
1201
962
  }
@@ -1208,11 +969,23 @@ class Modal extends Base {
1208
969
  }
1209
970
  _syncDialogState() {
1210
971
  const dialog = this._dialog;
1211
- if (!dialog)
972
+ if (!dialog || !this.isConnected)
1212
973
  return;
1213
974
  if (this.open) {
1214
- if (isInTopLayer(dialog))
975
+ this._closeReason = modalCloseReasonValues.API;
976
+ this._returnValue = "";
977
+ if (this._closing) {
978
+ this._closing = false;
979
+ this._closeRun += 1;
980
+ if (isInTopLayer(dialog)) {
981
+ this._animateDialog(dialog, true);
982
+ return;
983
+ }
984
+ }
985
+ if (isInTopLayer(dialog)) {
986
+ this._acquireScrollLock();
1215
987
  return;
988
+ }
1216
989
  this._openDialog(dialog);
1217
990
  } else if (dialog.open) {
1218
991
  this._runCloseSequence(dialog);
@@ -1222,25 +995,38 @@ class Modal extends Base {
1222
995
  if (dialog.open)
1223
996
  dialog.close();
1224
997
  this._closing = false;
1225
- this._previouslyFocused = deepActiveElement();
998
+ this._previouslyFocused ??= deepActiveElement();
1226
999
  dialog.returnValue = "";
1227
1000
  dialog.showModal();
1228
1001
  this._acquireScrollLock();
1229
1002
  this._focusInitialElement(dialog);
1003
+ this._animateDialog(dialog, true);
1230
1004
  this.emit("dsgn-modal-open");
1231
1005
  }
1232
1006
  async _runCloseSequence(dialog) {
1233
1007
  if (this._closing)
1234
1008
  return;
1235
1009
  this._closing = true;
1236
- await this.updateComplete;
1237
- await whenAnimationsSettle(dialog);
1010
+ const run = ++this._closeRun;
1011
+ await this._animateDialog(dialog, false);
1012
+ if (run !== this._closeRun || !this.isConnected)
1013
+ return;
1238
1014
  if (!dialog.open || this.open) {
1239
1015
  this._closing = false;
1240
1016
  return;
1241
1017
  }
1242
1018
  dialog.close(this._returnValue);
1243
1019
  }
1020
+ _animateDialog(dialog, entering) {
1021
+ const preset = modalMotionPresets[this._motionOrigin];
1022
+ const play = entering ? this._motion.enter.bind(this._motion) : this._motion.exit.bind(this._motion);
1023
+ return Promise.all([
1024
+ play(dialog, preset),
1025
+ play(dialog, "fade", { pseudoElement: "::backdrop" })
1026
+ ]).then(() => {
1027
+ return;
1028
+ });
1029
+ }
1244
1030
  _focusInitialElement(dialog) {
1245
1031
  const target = this.querySelector("[autofocus]") ?? dialog.querySelector("[autofocus]") ?? dialog;
1246
1032
  target.focus();
@@ -1266,7 +1052,10 @@ class Modal extends Base {
1266
1052
  _handleNativeClose() {
1267
1053
  if (this._dialog?.open)
1268
1054
  return;
1055
+ this._closeRun += 1;
1269
1056
  this._closing = false;
1057
+ if (this._dialog)
1058
+ cancel(this._dialog);
1270
1059
  this.open = false;
1271
1060
  this._releaseScrollLock();
1272
1061
  this._restoreFocus();
@@ -1325,7 +1114,6 @@ class Modal extends Base {
1325
1114
  data-placement=${this.placement}
1326
1115
  data-motion=${this._motionOrigin}
1327
1116
  data-size=${this.size}
1328
- data-state=${this._closing ? "closed" : "open"}
1329
1117
  aria-label=${!this._hasHeading && this.ariaLabel ? this.ariaLabel : nothing}
1330
1118
  aria-labelledby=${this._hasHeading ? "modal-heading" : nothing}
1331
1119
  aria-describedby=${this._hasDescription ? "modal-description" : nothing}
@@ -1412,9 +1200,6 @@ __legacyDecorateClassTS([
1412
1200
  __legacyDecorateClassTS([
1413
1201
  property({ type: Boolean, reflect: true, attribute: "no-escape-dismiss" })
1414
1202
  ], Modal.prototype, "noEscapeDismiss", undefined);
1415
- __legacyDecorateClassTS([
1416
- state()
1417
- ], Modal.prototype, "_closing", undefined);
1418
1203
  __legacyDecorateClassTS([
1419
1204
  state()
1420
1205
  ], Modal.prototype, "_hasHeading", undefined);
@@ -1,3 +1,4 @@
1
+ import type { PresetName } from '@ds-gn/motion';
1
2
  /**
2
3
  * Where the dialog sits in the viewport. `center` is a classic modal; the four
3
4
  * edges are sheets/drawers that span that edge.
@@ -28,6 +29,8 @@ export declare const modalMotionValues: {
28
29
  readonly BOTTOM: "bottom";
29
30
  };
30
31
  export type ModalMotionType = (typeof modalMotionValues)[keyof typeof modalMotionValues];
32
+ /** Motion names select an enter/exit pair; placement remains independent. */
33
+ export declare const modalMotionPresets: Record<Exclude<ModalMotionType, 'inherit'>, PresetName>;
31
34
  export declare const modalMotionProps: import("lit").PropertyDeclaration<unknown, unknown>;
32
35
  /**
33
36
  * Size along the axis the placement grows on: width for `center`/`left`/`right`,
@@ -39,6 +39,14 @@ var modalMotionValues = {
39
39
  TOP: "top",
40
40
  BOTTOM: "bottom"
41
41
  };
42
+ var modalMotionPresets = {
43
+ center: "scaleIn",
44
+ left: "slideInLeft",
45
+ right: "slideInRight",
46
+ top: "slideInTop",
47
+ bottom: "slideInBottom",
48
+ none: "fade"
49
+ };
42
50
  var modalMotionProps = enumCreator(Object.values(modalMotionValues), "motion", String, { reflect: true });
43
51
  var modalSizeValues = {
44
52
  SM: "sm",
@@ -56,6 +64,7 @@ var modalCloseReasonValues = {
56
64
  };
57
65
  export {
58
66
  modalCloseReasonValues,
67
+ modalMotionPresets,
59
68
  modalMotionProps,
60
69
  modalMotionValues,
61
70
  modalPlacementProps,
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * `:host` is the only rule with no utility equivalent — the host is the
3
3
  * consumer's own `<dsgn-modal>` element, so there is no class we can put on it.
4
- * Everything else, including `::backdrop`, the `[data-*]` state selectors and
5
- * the enter/exit motion, is expressed as Tailwind utilities in
4
+ * Layout and backdrop styling are expressed as Tailwind utilities in
6
5
  * `dialogClasses()` below.
7
6
  */
8
7
  export declare const hostStyles: import("lit").CSSResult;
@@ -10,14 +9,11 @@ export declare const hostStyles: import("lit").CSSResult;
10
9
  * Every style the `<dialog>` needs, as one static utility list.
11
10
  *
12
11
  * Nothing here is conditional: the dialog carries `data-placement`,
13
- * `data-size`, `data-motion` and `data-state`, and the `data-[…]:` variants
12
+ * `data-size`, and the `data-[…]:` variants
14
13
  * select which rules apply at runtime. That is also a requirement rather than a
15
14
  * preference — Tailwind only generates classes it finds as *literal* strings in
16
15
  * the source, so these cannot be composed or interpolated.
17
- *
18
- * Ordering note: `motion-reduce:` is emitted after the `data-motion` rules and
19
- * matches their specificity, so it wins — the reduced-motion cross-fade
20
- * overrides whichever spatial animation the placement selected.
16
+ * Motion is played by MotionController, independently of these styles.
21
17
  */
22
18
  export declare const dialogClasses: () => string;
23
19
  export declare const panelClasses: () => string;
@@ -489,6 +489,11 @@ dsgn-hero:not(:defined)[layout='asset-background']:not(:has(> [slot='features'])
489
489
  padding-bottom: calc(var(--spacing) * 6);
490
490
  grid-row: 3;
491
491
  }
492
+ @media (prefers-reduced-motion: no-preference) {
493
+ dsgn-hero:not(:defined)[motion='on'] > [slot='content'] [slot='eyebrow'] {
494
+ opacity: 0;
495
+ }
496
+ }
492
497
  @property --tw-tracking {
493
498
  syntax: "*";
494
499
  inherits: false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-gn/ui",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "UI components for the DS-GN Design System",
5
5
  "type": "module",
6
6
  "exports": {
@@ -308,11 +308,11 @@
308
308
  "generate:playground": "bun build/generate-playground-manifest.ts",
309
309
  "check:playground-manifest": "bun build/generate-playground-manifest.ts --check",
310
310
  "generate:react": "bun run generate:cem && bun run generate:playground && bun build/generate-react.ts",
311
- "build": "bun run --cwd ../tokens build && bun build/build-all-components.ts",
311
+ "build": "bun run --cwd ../tokens build && bun run --cwd ../motion build && bun build/build-all-components.ts",
312
312
  "build:css": "bun run --cwd ../tokens build && bun build/generate-component-css.ts",
313
313
  "test": "bun run build && bun test --conditions=browser",
314
- "build:dev": "bun run --cwd ../tokens build && cross-env NODE_ENV=development bun build/build-all-components.ts",
315
- "build:prod": "bun run --cwd ../tokens build && cross-env NODE_ENV=production bun build/build-all-components.ts",
314
+ "build:dev": "bun run --cwd ../tokens build && bun run --cwd ../motion build && cross-env NODE_ENV=development bun build/build-all-components.ts",
315
+ "build:prod": "bun run --cwd ../tokens build && bun run --cwd ../motion build && cross-env NODE_ENV=production bun build/build-all-components.ts",
316
316
  "watch": "cross-env NODE_ENV=development bun build/watch.ts",
317
317
  "update:hash": "bun build/update-package-hash.ts"
318
318
  },
@@ -346,7 +346,8 @@
346
346
  "react-dom": "19.2.4"
347
347
  },
348
348
  "dependencies": {
349
- "@ds-gn/tokens": "0.7.0",
349
+ "@ds-gn/motion": "0.9.0",
350
+ "@ds-gn/tokens": "0.8.0",
350
351
  "@lit-labs/observers": "^2.0.5",
351
352
  "@lit/react": "^1.0.7",
352
353
  "clsx": "^2.1.1"
@@ -1,9 +0,0 @@
1
- /**
2
- * Whether the user has requested reduced motion via the OS/browser setting.
3
- *
4
- * Used as a gate for auto-animating controllers (e.g. AutoRotateController,
5
- * WordCycleController) so motion respects `prefers-reduced-motion: reduce`.
6
- * Guarded for environments without `matchMedia` (SSR, test DOMs), where it
7
- * reports `false` (motion allowed) rather than throwing.
8
- */
9
- export declare function prefersReducedMotion(): boolean;