@juspay/svelte-ui-components 4.31.3 → 4.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/Accordion/Accordion.svelte +6 -1
  2. package/dist/Animations/ModalAnimation.svelte +71 -8
  3. package/dist/Animations/OverlayAnimation.svelte +29 -4
  4. package/dist/Animations/tokenizedFly.d.ts +17 -0
  5. package/dist/Animations/tokenizedFly.js +153 -0
  6. package/dist/AreaChart/AreaChart.svelte +7 -5
  7. package/dist/BarChart/BarChart.svelte +2 -1
  8. package/dist/Book/Book.svelte +6 -3
  9. package/dist/ChatBubble/ChatBubble.svelte +18 -3
  10. package/dist/ChatComposer/ChatComposer.svelte +4 -0
  11. package/dist/Checkbox/Checkbox.svelte +15 -3
  12. package/dist/Choicebox/Choicebox.svelte +24 -2
  13. package/dist/Combobox/Combobox.svelte +15 -1
  14. package/dist/CommandMenu/CommandMenu.svelte +72 -2
  15. package/dist/ContextMenu/ContextMenu.svelte +41 -1
  16. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +4 -3
  17. package/dist/FunnelChart/FunnelChart.svelte +5 -4
  18. package/dist/Gallery/Gallery.svelte +53 -1
  19. package/dist/Gallery/properties.d.ts +5 -3
  20. package/dist/Gauge/Gauge.svelte +2 -1
  21. package/dist/LineChart/LineChart.svelte +7 -5
  22. package/dist/MediaPlayer/MediaPlayer.svelte +9 -1
  23. package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +1 -1
  24. package/dist/MediaUpload/MediaUpload.svelte +1 -1
  25. package/dist/Menu/Menu.svelte +38 -1
  26. package/dist/Modal/Modal.svelte +18 -4
  27. package/dist/Pagination/Pagination.svelte +9 -1
  28. package/dist/PieChart/PieChart.svelte +2 -2
  29. package/dist/Radio/Radio.svelte +14 -2
  30. package/dist/RatingGroup/RatingGroup.svelte +9 -0
  31. package/dist/SankeyChart/SankeyChart.svelte +6 -3
  32. package/dist/Select/Select.svelte +26 -4
  33. package/dist/Sheet/Sheet.svelte +34 -2
  34. package/dist/Slider/Slider.svelte +26 -5
  35. package/dist/Snippet/Snippet.svelte +7 -1
  36. package/dist/Table/Table.svelte +10 -2
  37. package/dist/Tabs/Tabs.svelte +27 -4
  38. package/dist/TaskList/TaskList.svelte +10 -2
  39. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +13 -6
  40. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +5 -3
  41. package/dist/Toast/Toast.svelte +216 -116
  42. package/dist/Toggle/Toggle.svelte +8 -4
  43. package/dist/ToolCallLog/ToolCallLog.svelte +10 -2
  44. package/dist/Tooltip/Tooltip.svelte +4 -2
  45. package/dist-wc/index.js +608 -413
  46. package/package.json +1 -1
@@ -4,6 +4,7 @@
4
4
  import { lockBodyScroll, unlockBodyScroll } from '../utils';
5
5
  import { registerDismissible } from '../_interaction/dismissal';
6
6
  import { getActiveElement } from '../_interaction/focus';
7
+ import { tokenizedFly } from '../Animations/tokenizedFly';
7
8
  import { SvelteMap } from 'svelte/reactivity';
8
9
  import Img from '../Img/Img.svelte';
9
10
  import searchSvg from '../assets/search.svg?raw';
@@ -25,6 +26,27 @@
25
26
  const uid = $props.id();
26
27
  const listboxId = `command-menu-listbox-${uid}`;
27
28
 
29
+ // Centered, medium-large panel: --distance-large / --duration-base tiers —
30
+ // a command palette warrants more presence than a small anchored dropdown.
31
+ // 200ms matches --duration-base exactly, so the duration chain picks up that
32
+ // named tier before falling to --motion-duration. easingTokens is new:
33
+ // tokenizedFly now parses a CSS easing-token string (keyword or cubic-bezier)
34
+ // into a JS EasingFunction, so --command-menu-panel-transition-easing has a
35
+ // live hook -- --ease-smooth-out is the shared named tier, cubicOut stays the
36
+ // ultimate JS fallback if no token resolves.
37
+ const panelTransitionParams = {
38
+ y: 30,
39
+ durationTokens: [
40
+ '--command-menu-panel-transition-duration',
41
+ '--duration-base',
42
+ '--motion-duration'
43
+ ],
44
+ fallbackDuration: 200,
45
+ distanceTokens: ['--command-menu-panel-transition-distance', '--distance-large'],
46
+ fallbackDistance: 30,
47
+ easingTokens: ['--command-menu-panel-transition-easing', '--ease-smooth-out', '--motion-easing']
48
+ };
49
+
28
50
  let query = $state('');
29
51
  let activeIndex = $state(0);
30
52
  let inputElement: HTMLInputElement | null = $state(null);
@@ -86,7 +108,14 @@
86
108
  flatItems.length > 0 ? `command-menu-option-${uid}-${activeIndex}` : null
87
109
  );
88
110
 
111
+ // Guarded rather than assumed single-fire: out:tokenizedFly keeps the dialog
112
+ // (and its handlers) mounted for the outro's duration, so a second Escape,
113
+ // outside click, or item interaction landing during that window would
114
+ // otherwise fire onclose/onselect again before Svelte actually tears it down.
89
115
  function close() {
116
+ if (!open) {
117
+ return;
118
+ }
90
119
  open = false;
91
120
  query = '';
92
121
  activeIndex = 0;
@@ -94,7 +123,7 @@
94
123
  }
95
124
 
96
125
  function selectItem(item: CommandItem) {
97
- if (item.disabled) {
126
+ if (!open || item.disabled) {
98
127
  return;
99
128
  }
100
129
  onselect?.(item);
@@ -228,6 +257,23 @@
228
257
  // point in a fresh open. Ctrl+K stays on `handleGlobalKeyDown`/`window`
229
258
  // below rather than moving here: it is an OPEN shortcut that must keep
230
259
  // working while the menu is closed, and this layer only exists while open.
260
+ // KNOWN LIMITATION, not yet fixed: this action's mount function -- and so
261
+ // this capture -- only runs once, when `.command-menu-overlay` is first
262
+ // created. `open` is `$bindable`, so an external trigger button and the
263
+ // built-in Ctrl+K shortcut commonly coexist; if the menu is reopened via a
264
+ // DIFFERENT trigger while still mid-outro from a just-finished close
265
+ // (out:tokenizedFly's ~200ms fallback keeps this instance alive, and Svelte
266
+ // reverses rather than recreates the block on a same-window reopen), this
267
+ // function does not run again -- `openerElement` stays whatever the FIRST
268
+ // opener was, and focus incorrectly returns there instead of to the trigger
269
+ // that actually opened the currently-visible session. A correct fix needs
270
+ // to re-capture on every logical open, not just the first DOM mount (e.g. an
271
+ // `$effect` watching `open`), while still finding the right shadow-DOM-aware
272
+ // active element (getActiveElement needs an already-connected node, and this
273
+ // component renders nothing while closed) and without racing the focus-the-
274
+ // input logic below it. Deferred rather than shipped half-verified: a wrong
275
+ // guess here risks a worse regression (stealing focus into the wrong
276
+ // element on every open) than the narrow bug it would fix.
231
277
  function scrollLockAction(node: HTMLElement) {
232
278
  const opener = getActiveElement(node);
233
279
  if (openerElement === null && opener instanceof HTMLElement) {
@@ -267,6 +313,23 @@
267
313
  </script>
268
314
 
269
315
  {#if open}
316
+ <!-- `open` flips false (and aria-expanded on the trigger follows suit) the
317
+ instant close()/selectItem() run, but this whole subtree stays mounted
318
+ for .command-menu-dialog's out:tokenizedFly outro below -- Svelte only
319
+ destroys an ancestor once a descendant's own out-transition finishes.
320
+ `inert` pulls the fading-but-still-mounted dialog out of the
321
+ accessibility tree (and out of tab order) on that same instant, so a
322
+ screen reader doesn't still see role="dialog" aria-modal="true" for a
323
+ surface that's already reported itself closed. Set on BOTH this element
324
+ and .command-menu-dialog below, not just the one carrying
325
+ role="dialog": verified live that Svelte stops applying reactive
326
+ attribute updates to an ancestor once a descendant's own outro begins,
327
+ so the binding here alone would go stale the moment the transition
328
+ starts -- only the element actually running the out: transition is
329
+ guaranteed to keep updating through it. close()/selectItem() are
330
+ already guarded against a stray interaction landing here regardless --
331
+ this is the matching fix for what AT perceives, not a second copy of
332
+ that guard. -->
270
333
  <div
271
334
  class="command-menu-overlay {classes ?? ''}"
272
335
  use:scrollLockAction
@@ -276,10 +339,17 @@
276
339
  aria-modal="true"
277
340
  aria-label="Command menu"
278
341
  tabindex="-1"
342
+ inert={!open}
279
343
  data-pw={typeof testId === 'string' ? testId : null}
280
344
  testID={typeof testId === 'string' ? testId : null}
281
345
  >
282
- <div class="command-menu-dialog" bind:this={dialogElement}>
346
+ <div
347
+ class="command-menu-dialog"
348
+ bind:this={dialogElement}
349
+ inert={!open}
350
+ in:tokenizedFly={panelTransitionParams}
351
+ out:tokenizedFly={panelTransitionParams}
352
+ >
283
353
  <div class="command-menu-input-wrapper">
284
354
  {#if typeof searchIcon === 'function'}
285
355
  {@render searchIcon()}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { registerDismissible } from '../_interaction/dismissal';
3
3
  import { getActiveElement } from '../_interaction/focus';
4
+ import { tokenizedFly } from '../Animations/tokenizedFly';
4
5
  import type { ContextMenuProperties, ContextMenuItem } from './properties';
5
6
 
6
7
  let {
@@ -25,6 +26,27 @@
25
26
  // the menu goes away, matching Menu.svelte's own restore-on-close contract.
26
27
  let openerElement: HTMLElement | null = $state(null);
27
28
 
29
+ // Small anchored dropdown: --distance-medium / --duration-quick tiers — snappier
30
+ // and less travel than CommandMenu's centered palette, matching Menu.svelte's own.
31
+ // 150ms matches --duration-quick exactly, so the duration chain picks up that
32
+ // named tier before falling to --motion-duration. easingTokens is new:
33
+ // tokenizedFly now parses a CSS easing-token string (keyword or cubic-bezier)
34
+ // into a JS EasingFunction, so --context-menu-panel-transition-easing has a
35
+ // live hook -- --ease-smooth-out is the shared named tier, cubicOut stays the
36
+ // ultimate JS fallback if no token resolves.
37
+ const panelTransitionParams = {
38
+ y: 12,
39
+ durationTokens: [
40
+ '--context-menu-panel-transition-duration',
41
+ '--duration-quick',
42
+ '--motion-duration'
43
+ ],
44
+ fallbackDuration: 150,
45
+ distanceTokens: ['--context-menu-panel-transition-distance', '--distance-medium'],
46
+ fallbackDistance: 12,
47
+ easingTokens: ['--context-menu-panel-transition-easing', '--ease-smooth-out', '--motion-easing']
48
+ };
49
+
28
50
  let selectableItems: ContextMenuItem[] = $derived(
29
51
  items.filter((item) => !item.separator && !item.disabled)
30
52
  );
@@ -57,7 +79,14 @@
57
79
  });
58
80
  }
59
81
 
82
+ // Guarded rather than assumed single-fire: out:tokenizedFly keeps the dropdown
83
+ // (and its handlers) mounted for the outro's duration, so a second Escape,
84
+ // outside click, or item interaction landing during that window would
85
+ // otherwise fire onclose/onselect again before Svelte actually tears it down.
60
86
  function close() {
87
+ if (!open) {
88
+ return;
89
+ }
61
90
  cancelOpenFrame();
62
91
  open = false;
63
92
  focusedIndex = -1;
@@ -83,7 +112,7 @@
83
112
  }
84
113
 
85
114
  function selectMenuItem(item: ContextMenuItem) {
86
- if (item.disabled) {
115
+ if (!open || item.disabled) {
87
116
  return;
88
117
  }
89
118
  onselect?.(item);
@@ -225,14 +254,25 @@
225
254
  </div>
226
255
 
227
256
  {#if open}
257
+ <!-- `open` flips false the instant close()/selectMenuItem() run, but this
258
+ subtree stays mounted for the out:tokenizedFly outro below -- Svelte
259
+ only destroys it once its own out-transition finishes. `inert` pulls
260
+ the fading-but-still-mounted dropdown out of the accessibility tree
261
+ (and tab order) on that same instant, matching what the trigger's own
262
+ state already reports. close()/selectMenuItem() are already guarded
263
+ against a stray interaction landing here regardless -- this is the
264
+ matching fix for what AT perceives, not a second copy of that guard. -->
228
265
  <div
229
266
  class="context-menu-dropdown"
230
267
  style="left: {posX}px; top: {posY}px; --context-menu-max-height: {maxHeight};"
231
268
  bind:this={menuEl}
232
269
  role="menu"
233
270
  tabindex="-1"
271
+ inert={!open}
234
272
  onkeydown={handleMenuKeydown}
235
273
  use:dismissalAction
274
+ in:tokenizedFly={panelTransitionParams}
275
+ out:tokenizedFly={panelTransitionParams}
236
276
  >
237
277
  {#each items as item (item.value)}
238
278
  {#if item.separator}
@@ -655,7 +655,8 @@
655
655
  }
656
656
 
657
657
  .bar-shape {
658
- transition: opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
658
+ transition: opacity
659
+ var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
659
660
  var(--chart-transition-easing, var(--motion-easing, ease));
660
661
  cursor: pointer;
661
662
  }
@@ -677,9 +678,9 @@
677
678
 
678
679
  .line-dot {
679
680
  transition:
680
- r var(--chart-transition-duration, var(--motion-duration, 0.2s))
681
+ r var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
681
682
  var(--chart-transition-easing, var(--motion-easing, ease)),
682
- opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
683
+ opacity var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
683
684
  var(--chart-transition-easing, var(--motion-easing, ease));
684
685
  cursor: pointer;
685
686
  }
@@ -431,11 +431,11 @@
431
431
 
432
432
  .funnel-bar {
433
433
  transition:
434
- opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
434
+ opacity var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
435
435
  var(--chart-transition-easing, var(--motion-easing, ease)),
436
- y var(--chart-transition-duration, var(--motion-duration, 0.2s))
436
+ y var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
437
437
  var(--chart-transition-easing, var(--motion-easing, ease)),
438
- height var(--chart-transition-duration, var(--motion-duration, 0.2s))
438
+ height var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
439
439
  var(--chart-transition-easing, var(--motion-easing, ease));
440
440
  cursor: pointer;
441
441
  }
@@ -463,7 +463,8 @@
463
463
  }
464
464
 
465
465
  .funnel-connector {
466
- transition: d var(--chart-transition-duration, var(--motion-duration, 0.2s))
466
+ transition: d
467
+ var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
467
468
  var(--chart-transition-easing, var(--motion-easing, ease));
468
469
  }
469
470
 
@@ -79,6 +79,54 @@
79
79
  };
80
80
  }
81
81
 
82
+ /**
83
+ * Resolves a CSS time value (e.g. `"0.2s"`, `"150ms"`) to milliseconds.
84
+ * Returns `null` rather than 0 when nothing parses, so an explicit `0s`
85
+ * override is never confused with "no token set".
86
+ */
87
+ function parseCssDurationMs(value: string): number | null {
88
+ const text = value.trim();
89
+ if (text.length === 0) {
90
+ return null;
91
+ }
92
+ const amount = Number.parseFloat(text);
93
+ if (!Number.isFinite(amount)) {
94
+ return null;
95
+ }
96
+ if (text.endsWith('ms')) {
97
+ return amount;
98
+ }
99
+ if (text.endsWith('s')) {
100
+ return amount * 1000;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Wraps `fade` so the lightbox honours the CSS token chain
107
+ * (`--gallery-lightbox-transition-duration` / `--duration-base` /
108
+ * `--motion-duration`, resolved once on `.lightbox` below as
109
+ * `--_gallery-lightbox-transition-duration`) when a consumer sets one, and
110
+ * otherwise falls back to the `lightboxTransitionDuration` prop --
111
+ * unchanged default of 200ms when NEITHER is set. An explicit prop wins over
112
+ * the CSS token when BOTH are set, not the other way around: `--motion-duration`
113
+ * is an ambient, root-level default meant for a consumer who never touches
114
+ * this component directly, while `lightboxTransitionDuration` is an explicit,
115
+ * per-instance choice -- the more specific override has to win, the same way
116
+ * an inline style beats a stylesheet rule. `typeof … === 'number'` (not `??`)
117
+ * is what makes that precedence real: `??` would let the CSS token jump the
118
+ * queue the moment a consumer sets one globally, silently overriding a prop
119
+ * they explicitly passed to this specific instance.
120
+ */
121
+ function lightboxFade(node: Element) {
122
+ if (typeof lightboxTransitionDuration === 'number') {
123
+ return fade(node, { duration: lightboxTransitionDuration });
124
+ }
125
+ const raw = getComputedStyle(node).getPropertyValue('--_gallery-lightbox-transition-duration');
126
+ const duration = parseCssDurationMs(raw) ?? 200;
127
+ return fade(node, { duration });
128
+ }
129
+
82
130
  function openLightbox(index: number, opener: EventTarget | null): void {
83
131
  if (opener instanceof HTMLElement) {
84
132
  openerElement = opener;
@@ -273,7 +321,7 @@
273
321
  use:lightboxAction
274
322
  onclick={handleBackdropClick}
275
323
  onkeydown={handleLightboxKeydown}
276
- transition:fade={{ duration: lightboxTransitionDuration ?? 200 }}
324
+ transition:lightboxFade
277
325
  >
278
326
  <div class="lightbox-close" bind:this={closeButtonWrap}>
279
327
  <Button ariaLabel="Close gallery" onclick={closeLightbox}>
@@ -527,6 +575,10 @@
527
575
  align-items: center;
528
576
  background: var(--gallery-lightbox-background, #000000e6);
529
577
  -webkit-tap-highlight-color: transparent;
578
+ --_gallery-lightbox-transition-duration: var(
579
+ --gallery-lightbox-transition-duration,
580
+ var(--duration-base, var(--motion-duration))
581
+ );
530
582
  }
531
583
 
532
584
  .lightbox:focus {
@@ -28,9 +28,11 @@ export type OptionalGalleryProperties = {
28
28
  testId?: string;
29
29
  classes?: string;
30
30
  /**
31
- * Duration (ms) of the lightbox's `fade` transition. A CSS custom property
32
- * cannot reach a Svelte transition directive's parameters, so this prop is
33
- * the token-contract equivalent for Gallery's motion — same shape as
31
+ * Duration (ms) of the lightbox's `fade` transition, used whenever the
32
+ * `--gallery-lightbox-transition-duration` CSS token (and its
33
+ * `--duration-base` / `--motion-duration` fallbacks) resolve to nothing.
34
+ * A transition directive's parameters are plain JS, so `Gallery.svelte`
35
+ * bridges the token chain in via `getComputedStyle` — same shape as
34
36
  * Toast's `inAnimationDuration`/`outAnimationDuration`. Defaults to the
35
37
  * library's existing 200ms so omitting it renders identically to before.
36
38
  */
@@ -99,7 +99,8 @@
99
99
  .bar {
100
100
  stroke: var(--gauge-bar-color, #2196f3);
101
101
  stroke-width: var(--gauge-stroke-width, 8);
102
- transition: stroke-dashoffset var(--gauge-transition-duration, 0.3s) ease;
102
+ transition: stroke-dashoffset var(--gauge-transition-duration, var(--motion-duration, 0.3s))
103
+ var(--gauge-transition-easing, var(--motion-easing, ease));
103
104
  }
104
105
 
105
106
  .label {
@@ -1062,9 +1062,10 @@
1062
1062
  }
1063
1063
  .line-area-fill {
1064
1064
  transition:
1065
- fill-opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
1065
+ fill-opacity
1066
+ var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
1066
1067
  var(--chart-transition-easing, var(--motion-easing, ease)),
1067
- opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
1068
+ opacity var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
1068
1069
  var(--chart-transition-easing, var(--motion-easing, ease));
1069
1070
  pointer-events: none;
1070
1071
  }
@@ -1072,7 +1073,8 @@
1072
1073
  opacity: var(--linechart-dimmed-opacity, 0.2);
1073
1074
  }
1074
1075
  .line-path {
1075
- transition: opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
1076
+ transition: opacity
1077
+ var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
1076
1078
  var(--chart-transition-easing, var(--motion-easing, ease));
1077
1079
  stroke-linecap: round;
1078
1080
  stroke-linejoin: round;
@@ -1089,9 +1091,9 @@
1089
1091
  }
1090
1092
  .dot {
1091
1093
  transition:
1092
- r var(--chart-transition-duration, var(--motion-duration, 0.2s))
1094
+ r var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
1093
1095
  var(--chart-transition-easing, var(--motion-easing, ease)),
1094
- opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
1096
+ opacity var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
1095
1097
  var(--chart-transition-easing, var(--motion-easing, ease));
1096
1098
  stroke: var(--chart-dot-stroke, light-dark(#fff, #111827));
1097
1099
  stroke-width: 2;
@@ -455,7 +455,15 @@
455
455
  z-index: var(--media-player-overlay-z-index, 20);
456
456
  border-radius: inherit;
457
457
  background-color: var(--media-player-overlay-color, transparent);
458
- transition: var(--media-player-overlay-transition, background-color 0.2s ease);
458
+ transition: var(
459
+ --media-player-overlay-transition,
460
+ background-color
461
+ var(
462
+ --media-player-overlay-transition-duration,
463
+ var(--duration-base, var(--motion-duration, 0.2s))
464
+ )
465
+ var(--media-player-overlay-transition-easing, var(--motion-easing, ease))
466
+ );
459
467
  --center-controls-visibility: var(--media-player-center-controls-visibility, hidden);
460
468
  --bottom-controls-visibility: var(--media-player-bottom-controls-visibility, hidden);
461
469
  }
@@ -1,4 +1,4 @@
1
1
  import type { MediaPlayerProperties } from './properties';
2
- declare const MediaPlayer: import("svelte").Component<MediaPlayerProperties, {}, "muted" | "duration" | "playing" | "currentTime">;
2
+ declare const MediaPlayer: import("svelte").Component<MediaPlayerProperties, {}, "muted" | "playing" | "duration" | "currentTime">;
3
3
  type MediaPlayer = ReturnType<typeof MediaPlayer>;
4
4
  export default MediaPlayer;
@@ -542,7 +542,7 @@
542
542
  consumer overriding the token is most likely adding more of it. Dropping the
543
543
  whole declaration is right here because every end state is a resting state. */
544
544
  @media (prefers-reduced-motion: reduce) {
545
- .media-item {
545
+ .card {
546
546
  transition: none;
547
547
  }
548
548
  }
@@ -4,6 +4,7 @@
4
4
  import Img from '../Img/Img.svelte';
5
5
  import { computeMenuDropdownPosition } from './dropdownPosition';
6
6
  import { eventHitsInside, registerDismissible } from '../_interaction/dismissal';
7
+ import { tokenizedFly } from '../Animations/tokenizedFly';
7
8
  import type { MenuProperties, MenuItem, MenuPlacement } from './properties';
8
9
 
9
10
  let {
@@ -79,6 +80,23 @@
79
80
  // Gap between trigger and portaled panel, matching the --menu-margin default.
80
81
  const PORTAL_MENU_GAP = 4;
81
82
 
83
+ // Small anchored dropdown: --distance-medium / --duration-quick tiers — snappier
84
+ // and less travel than CommandMenu's centered palette. 150ms matches
85
+ // --duration-quick exactly, so the duration chain picks up that named tier
86
+ // before falling to --motion-duration. easingTokens is new: tokenizedFly now
87
+ // parses a CSS easing-token string (keyword or cubic-bezier) into a JS
88
+ // EasingFunction, so --menu-panel-transition-easing has a live hook --
89
+ // --ease-smooth-out is the shared named tier, cubicOut stays the ultimate JS
90
+ // fallback if no token resolves.
91
+ const panelTransitionParams = {
92
+ y: 12,
93
+ durationTokens: ['--menu-panel-transition-duration', '--duration-quick', '--motion-duration'],
94
+ fallbackDuration: 150,
95
+ distanceTokens: ['--menu-panel-transition-distance', '--distance-medium'],
96
+ fallbackDistance: 12,
97
+ easingTokens: ['--menu-panel-transition-easing', '--ease-smooth-out', '--motion-easing']
98
+ };
99
+
82
100
  // Keep the portaled panel anchored to its trigger while the page scrolls or
83
101
  // resizes. Mirrors the chart-tooltip portal pattern; $effect is the sanctioned
84
102
  // reactive escape hatch here for untracked window listeners. Reposition work is
@@ -213,7 +231,15 @@
213
231
  });
214
232
  }
215
233
 
234
+ // Guarded rather than assumed single-fire: out:tokenizedFly keeps the dropdown
235
+ // (and its handlers) mounted for the outro's duration, so a second Escape,
236
+ // outside click, or item interaction landing during that window would
237
+ // otherwise fire onclose/onselect (and re-run focus restoration) again before
238
+ // Svelte actually tears it down.
216
239
  function close() {
240
+ if (!open) {
241
+ return;
242
+ }
217
243
  open = false;
218
244
  focusedIndex = -1;
219
245
  typeaheadQuery = '';
@@ -243,7 +269,7 @@
243
269
  }
244
270
 
245
271
  function selectItem(item: MenuItem) {
246
- if (item.disabled === true) {
272
+ if (!open || item.disabled === true) {
247
273
  return;
248
274
  }
249
275
  onselect?.(item);
@@ -453,6 +479,14 @@
453
479
  {/if}
454
480
 
455
481
  {#if open}
482
+ <!-- `open` flips false the instant close()/selectItem() run, but this
483
+ subtree stays mounted for the out:tokenizedFly outro below -- Svelte
484
+ only destroys it once its own out-transition finishes. `inert` pulls
485
+ the fading-but-still-mounted dropdown out of the accessibility tree
486
+ (and tab order) on that same instant, matching what aria-expanded on
487
+ the trigger already reports. close()/selectItem() are already guarded
488
+ against a stray interaction landing here regardless -- this is the
489
+ matching fix for what AT perceives, not a second copy of that guard. -->
456
490
  <div
457
491
  class="menu-dropdown menu-dropdown-{placement === 'auto' ? resolvedPlacement : placement}"
458
492
  class:menu-dropdown-measuring={measuringPlacement}
@@ -463,10 +497,13 @@
463
497
  id={menuId}
464
498
  aria-label={menuAriaLabel}
465
499
  tabindex="-1"
500
+ inert={!open}
466
501
  style={portalStyle}
467
502
  onkeydown={handleMenuKeydown}
468
503
  use:portalToRoot
469
504
  use:dismissalAction
505
+ in:tokenizedFly={panelTransitionParams}
506
+ out:tokenizedFly={panelTransitionParams}
470
507
  >
471
508
  {#each items as item (item.value)}
472
509
  {#if item.separator === true}
@@ -106,10 +106,18 @@
106
106
  };
107
107
  };
108
108
 
109
+ // Debounced through the same leading-edge debounce() handleOverlayClick
110
+ // already uses below, for the same reason: OverlayAnimation's out:tokenizedFly
111
+ // keeps the popstate listener registered (onMount/onDestroy span the outro,
112
+ // ~350ms fallback) for as long as the overlay is still fading out, so a
113
+ // second rapid back-press in that window would otherwise fire onclose/
114
+ // ondismiss a second time for one dismissal.
109
115
  const handlePopstate = (): void => {
110
116
  backPressed = true;
111
- onclose?.();
112
- ondismiss?.();
117
+ debounce(() => {
118
+ onclose?.();
119
+ ondismiss?.();
120
+ });
113
121
  };
114
122
 
115
123
  const handleRightImageClick = (event: MouseEvent): void => {
@@ -161,9 +169,15 @@
161
169
  // dismissible layer and Escape is pressed -- same callbacks the old
162
170
  // Escape branch in handleKeyDown used to invoke directly, just gated on
163
171
  // ownership now instead of firing for every open modal unconditionally.
172
+ // Debounced through the same debounce() handleOverlayClick/handlePopstate
173
+ // use, for the same reason: a second Escape landing while
174
+ // OverlayAnimation's out:tokenizedFly outro is still keeping this modal's
175
+ // dismissal listeners registered would otherwise fire the callbacks twice.
164
176
  const handleEscape = (): void => {
165
- onoverlayclick?.();
166
- ondismiss?.();
177
+ debounce(() => {
178
+ onoverlayclick?.();
179
+ ondismiss?.();
180
+ });
167
181
  };
168
182
 
169
183
  // The overlay's own Enter/Space activation, additional to the shared
@@ -159,7 +159,15 @@
159
159
  display: flex;
160
160
  align-items: center;
161
161
  justify-content: center;
162
- transition: var(--pagination-transition, background 0.15s ease, color 0.15s ease);
162
+ transition: var(
163
+ --pagination-transition,
164
+ background
165
+ var(--pagination-transition-duration, var(--duration-quick, var(--motion-duration, 0.15s)))
166
+ var(--pagination-transition-easing, var(--motion-easing, ease)),
167
+ color
168
+ var(--pagination-transition-duration, var(--duration-quick, var(--motion-duration, 0.15s)))
169
+ var(--pagination-transition-easing, var(--motion-easing, ease))
170
+ );
163
171
  }
164
172
 
165
173
  .page-button:hover:not(:disabled):not(.active) {
@@ -662,9 +662,9 @@
662
662
  stroke: var(--piechart-stroke-color, #fff);
663
663
  stroke-width: var(--piechart-stroke-width, 2);
664
664
  transition:
665
- transform var(--chart-transition-duration, var(--motion-duration, 0.2s))
665
+ transform var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
666
666
  var(--chart-transition-easing, var(--motion-easing, ease)),
667
- opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
667
+ opacity var(--chart-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
668
668
  var(--chart-transition-easing, var(--motion-easing, ease));
669
669
  transform-origin: 0 0;
670
670
  cursor: pointer;
@@ -122,7 +122,15 @@
122
122
  border: var(--radio-border, 2px solid #9e9e9e);
123
123
  border-radius: var(--radio-border-radius, 50%);
124
124
  background-color: var(--radio-background, #ffffff);
125
- transition: var(--radio-transition, 0.2s);
125
+ /* --radio-transition wraps the WHOLE value, not just the duration -- see the
126
+ matching comment on Checkbox's .box rule for why appending a separate
127
+ easing term after it (the prior shape here) breaks a consumer override
128
+ that already includes its own timing function. */
129
+ transition: var(
130
+ --radio-transition,
131
+ var(--radio-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
132
+ var(--radio-transition-easing, var(--motion-easing, ease))
133
+ );
126
134
  box-sizing: border-box;
127
135
  flex-shrink: 0;
128
136
  }
@@ -150,7 +158,11 @@
150
158
  height: 0;
151
159
  border-radius: var(--radio-dot-border-radius, 50%);
152
160
  background-color: var(--radio-dot-color, #2196f3);
153
- transition: var(--radio-transition, 0.2s);
161
+ transition: var(
162
+ --radio-transition,
163
+ var(--radio-transition-duration, var(--duration-base, var(--motion-duration, 0.2s)))
164
+ var(--radio-transition-easing, var(--motion-easing, ease))
165
+ );
154
166
  }
155
167
 
156
168
  .radio-dot.checked {
@@ -240,12 +240,21 @@
240
240
  width: var(--rating-group-star-size, 24px);
241
241
  height: var(--rating-group-star-size, 24px);
242
242
  flex-shrink: 0;
243
+ transition: transform
244
+ var(--rating-group-star-transition-duration, var(--motion-duration, 0.15s))
245
+ var(--rating-group-star-transition-easing, var(--motion-easing, ease));
243
246
  }
244
247
 
245
248
  .rating-group:not(.disabled):not(.readonly) .star:hover {
246
249
  transform: var(--rating-group-star-hover-transform, scale(1.1));
247
250
  }
248
251
 
252
+ @media (prefers-reduced-motion: reduce) {
253
+ .star {
254
+ transition: none;
255
+ }
256
+ }
257
+
249
258
  .star-empty,
250
259
  .star-fill {
251
260
  position: absolute;