@ds-gn/ui 0.7.0 → 0.8.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,21 @@
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
+ },
1508
1523
  {
1509
1524
  "kind": "field",
1510
1525
  "name": "surface",
@@ -1591,6 +1606,12 @@
1591
1606
  "description": "Accessible label for the tab list.",
1592
1607
  "attribute": "tabs-aria-label"
1593
1608
  },
1609
+ {
1610
+ "kind": "method",
1611
+ "name": "_fadeInDetail",
1612
+ "privacy": "private",
1613
+ "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."
1614
+ },
1594
1615
  {
1595
1616
  "kind": "method",
1596
1617
  "name": "_handleTabClick",
@@ -3620,6 +3641,13 @@
3620
3641
  "attribute": "no-escape-dismiss",
3621
3642
  "reflects": true
3622
3643
  },
3644
+ {
3645
+ "kind": "field",
3646
+ "name": "_motion",
3647
+ "privacy": "private",
3648
+ "readonly": true,
3649
+ "default": "new MotionController(this)"
3650
+ },
3623
3651
  {
3624
3652
  "kind": "field",
3625
3653
  "name": "_closing",
@@ -3627,8 +3655,17 @@
3627
3655
  "text": "boolean"
3628
3656
  },
3629
3657
  "privacy": "private",
3630
- "default": "false",
3631
- "description": "Drives `data-state`, which the stylesheet keys the exit animation on."
3658
+ "default": "false"
3659
+ },
3660
+ {
3661
+ "kind": "field",
3662
+ "name": "_closeRun",
3663
+ "type": {
3664
+ "text": "number"
3665
+ },
3666
+ "privacy": "private",
3667
+ "default": "0",
3668
+ "description": "Invalidates an exit when reopened, disconnected, or closed externally."
3632
3669
  },
3633
3670
  {
3634
3671
  "kind": "field",
@@ -3724,7 +3761,7 @@
3724
3761
  "kind": "field",
3725
3762
  "name": "_motionOrigin",
3726
3763
  "type": {
3727
- "text": "string"
3764
+ "text": "Exclude<ModalMotionType, 'inherit'>"
3728
3765
  },
3729
3766
  "privacy": "private",
3730
3767
  "description": "The resolved animation origin, with `inherit` folded into `placement`.",
@@ -3800,6 +3837,30 @@
3800
3837
  ],
3801
3838
  "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
3839
  },
3840
+ {
3841
+ "kind": "method",
3842
+ "name": "_animateDialog",
3843
+ "privacy": "private",
3844
+ "return": {
3845
+ "type": {
3846
+ "text": "Promise<void>"
3847
+ }
3848
+ },
3849
+ "parameters": [
3850
+ {
3851
+ "name": "dialog",
3852
+ "type": {
3853
+ "text": "HTMLDialogElement"
3854
+ }
3855
+ },
3856
+ {
3857
+ "name": "entering",
3858
+ "type": {
3859
+ "text": "boolean"
3860
+ }
3861
+ }
3862
+ ]
3863
+ },
3803
3864
  {
3804
3865
  "kind": "method",
3805
3866
  "name": "_focusInitialElement",
@@ -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;
@@ -19,6 +19,9 @@ export declare class FeatureTabs extends Base {
19
19
  paddingBottom?: SectionSpaceType;
20
20
  private _inView;
21
21
  private _rotate;
22
+ private _motion;
23
+ /** The feature whose detail last faded in; the fade replays when it changes. */
24
+ private _shownIndex?;
22
25
  surface: SurfaceType;
23
26
  features: FeatureTabData[];
24
27
  duration: number;
@@ -37,6 +40,12 @@ export declare class FeatureTabs extends Base {
37
40
  constructor();
38
41
  firstUpdated(): void;
39
42
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
43
+ /**
44
+ * Fade the detail in whenever the active feature changes. `keyed` renders a
45
+ * fresh element per feature, so each change animates a new node. This runs
46
+ * after render and before paint, so the new detail never shows unfaded.
47
+ */
48
+ private _fadeInDetail;
40
49
  private _handleTabClick;
41
50
  private _handleTabKeyDown;
42
51
  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,9 @@ 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 { MotionController } from "@ds-gn/motion/lit";
222
+
232
223
  // ../../node_modules/.bun/lit-html@3.3.3/node_modules/lit-html/development/is-server.js
233
224
  var NODE_MODE = false;
234
225
  var isServer = NODE_MODE;
@@ -338,8 +329,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
338
329
  --color-text-primary: inherit;
339
330
  --color-text-secondary: inherit;
340
331
  --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
332
  }
344
333
  }
345
334
  @layer utilities {
@@ -463,12 +452,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
463
452
  .shrink-0 {
464
453
  flex-shrink: 0;
465
454
  }
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
455
  .cursor-pointer {
473
456
  cursor: pointer;
474
457
  }
@@ -850,22 +833,6 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
850
833
  syntax: "*";
851
834
  inherits: false;
852
835
  }
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
836
  @layer properties {
870
837
  @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
871
838
  *, ::before, ::after, ::backdrop {
@@ -1019,7 +986,7 @@ var patternOverlayStyles = () => ({
1019
986
  maskImage: PATTERN_MASK,
1020
987
  WebkitMaskImage: PATTERN_MASK
1021
988
  });
1022
- var detailContentClassNames = (idx) => clsx_default("relative", "flex", "flex-col", "flex-1", idx % 2 === 0 ? "animate-fade-in-a" : "animate-fade-in-b");
989
+ var detailContentClassNames = () => clsx_default("relative", "flex", "flex-col", "flex-1");
1023
990
  var detailHeaderClassNames = () => clsx_default("flex", "items-center", "gap-3", "mb-6");
1024
991
  var detailEyebrowClassNames = () => clsx_default("font-mono", "text-[11px]", "tracking-[0.06em]", "uppercase", "text-text-tertiary");
1025
992
  var detailCounterClassNames = () => clsx_default("ml-auto", "font-mono", "text-[12px]", "text-text-tertiary");
@@ -1069,6 +1036,7 @@ class FeatureTabs extends Base {
1069
1036
  config: { threshold: 0 },
1070
1037
  callback: (entries) => entries[entries.length - 1]?.isIntersecting ?? false
1071
1038
  });
1039
+ this._motion = new MotionController(this);
1072
1040
  this.surface = surfaceValues.DARK;
1073
1041
  this.features = DEFAULT_FEATURES;
1074
1042
  this.duration = DEFAULT_DURATION;
@@ -1089,6 +1057,14 @@ class FeatureTabs extends Base {
1089
1057
  if (changedProperties.has("duration")) {
1090
1058
  this._rotate.setDuration(this.duration);
1091
1059
  }
1060
+ this._fadeInDetail();
1061
+ }
1062
+ _fadeInDetail() {
1063
+ const index = this._rotate.activeIndex;
1064
+ if (index === this._shownIndex)
1065
+ return;
1066
+ this._shownIndex = index;
1067
+ this.renderRoot.querySelectorAll("[data-detail-content]").forEach((detail) => void this._motion.enter(detail, "fade"));
1092
1068
  }
1093
1069
  _handleTabClick(index) {
1094
1070
  this._rotate.goTo(index);
@@ -1148,7 +1124,7 @@ class FeatureTabs extends Base {
1148
1124
  <div aria-hidden="true" style=${styleMap(patternOverlayStyles())}></div>
1149
1125
 
1150
1126
  ${keyed(idx, html2`
1151
- <div class="${detailContentClassNames(idx)}">
1127
+ <div class="${detailContentClassNames()}" data-detail-content>
1152
1128
  <div class="${detailHeaderClassNames()}">
1153
1129
  <span class="${iconBadgeClassNames(true, true)}">
1154
1130
  ${this._renderIcon(feature.icon, "lg")}
@@ -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 {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-gn/ui",
3
- "version": "0.7.0",
3
+ "version": "0.8.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.8.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;