@juspay/svelte-ui-components 4.27.6 → 4.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +140 -36
  2. package/dist/Animations/ModalAnimation.svelte +15 -1
  3. package/dist/Animations/OverlayAnimation.svelte +7 -1
  4. package/dist/AreaChart/AreaChart.svelte +366 -85
  5. package/dist/AspectRatio/AspectRatio.svelte +51 -0
  6. package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
  7. package/dist/AspectRatio/properties.d.ts +27 -0
  8. package/dist/AspectRatio/properties.js +1 -0
  9. package/dist/Banner/Banner.svelte +10 -4
  10. package/dist/Banner/properties.d.ts +8 -0
  11. package/dist/BarChart/BarChart.svelte +156 -49
  12. package/dist/Book/Book.svelte +14 -1
  13. package/dist/BrandLoader/BrandLoader.svelte +76 -12
  14. package/dist/Button/Button.svelte +56 -11
  15. package/dist/Calendar/Calendar.svelte +88 -61
  16. package/dist/Card/Card.svelte +1 -1
  17. package/dist/Carousel/Carousel.svelte +6 -3
  18. package/dist/ChatComposer/ChatComposer.svelte +54 -4
  19. package/dist/ChatComposer/dictationState.d.ts +15 -0
  20. package/dist/ChatComposer/dictationState.js +9 -0
  21. package/dist/ChatComposer/properties.d.ts +28 -1
  22. package/dist/ChatMessage/ChatMessage.svelte +27 -1
  23. package/dist/CheckListItem/CheckListItem.svelte +3 -3
  24. package/dist/Checkbox/Checkbox.svelte +84 -3
  25. package/dist/Checkbox/properties.d.ts +45 -13
  26. package/dist/ChipInput/ChipInput.svelte +70 -3
  27. package/dist/ChipInput/properties.d.ts +16 -0
  28. package/dist/Choicebox/Choicebox.svelte +156 -1
  29. package/dist/Choicebox/group.d.ts +63 -0
  30. package/dist/Choicebox/group.js +127 -0
  31. package/dist/Choicebox/properties.d.ts +46 -0
  32. package/dist/ColorPicker/ColorPicker.svelte +169 -20
  33. package/dist/ColorPicker/properties.d.ts +16 -0
  34. package/dist/Combobox/Combobox.svelte +87 -25
  35. package/dist/Combobox/properties.d.ts +23 -0
  36. package/dist/CommandMenu/CommandMenu.svelte +27 -11
  37. package/dist/ContextMenu/ContextMenu.svelte +31 -37
  38. package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
  39. package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
  40. package/dist/Draggable/Draggable.svelte +12 -7
  41. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
  42. package/dist/EmptyState/EmptyState.svelte +1 -1
  43. package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
  44. package/dist/FileInput/FileInput.svelte +50 -1
  45. package/dist/FileInput/properties.d.ts +7 -0
  46. package/dist/FunnelChart/FunnelChart.svelte +40 -12
  47. package/dist/FunnelChart/properties.d.ts +7 -1
  48. package/dist/Gallery/Gallery.svelte +20 -13
  49. package/dist/Gallery/properties.d.ts +8 -0
  50. package/dist/GridItem/GridItem.svelte +31 -1
  51. package/dist/HITL/HITL.svelte +35 -9
  52. package/dist/Icon/Icon.svelte +19 -3
  53. package/dist/Input/Input.svelte +24 -10
  54. package/dist/Input/properties.d.ts +37 -1
  55. package/dist/InputButton/InputButton.svelte +86 -11
  56. package/dist/InputButton/properties.d.ts +10 -1
  57. package/dist/Label/Label.svelte +54 -0
  58. package/dist/Label/Label.svelte.d.ts +3 -0
  59. package/dist/Label/properties.d.ts +26 -0
  60. package/dist/Label/properties.js +1 -0
  61. package/dist/LineChart/LineChart.svelte +385 -94
  62. package/dist/ListItem/ListItem.svelte +121 -15
  63. package/dist/Loader/Loader.svelte +22 -2
  64. package/dist/LoadingDots/LoadingDots.svelte +21 -0
  65. package/dist/MediaUpload/MediaUpload.svelte +13 -3
  66. package/dist/Menu/Menu.svelte +56 -28
  67. package/dist/Modal/Modal.svelte +77 -42
  68. package/dist/Pagination/Pagination.svelte +9 -2
  69. package/dist/Phone/Phone.svelte +4 -0
  70. package/dist/PieChart/PieChart.svelte +282 -38
  71. package/dist/PieChart/properties.d.ts +9 -0
  72. package/dist/Pill/Pill.svelte +14 -5
  73. package/dist/Progress/Progress.svelte +20 -0
  74. package/dist/Radio/Radio.svelte +58 -1
  75. package/dist/Radio/properties.d.ts +23 -0
  76. package/dist/RatingGroup/RatingGroup.svelte +283 -0
  77. package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
  78. package/dist/RatingGroup/properties.d.ts +73 -0
  79. package/dist/RatingGroup/properties.js +1 -0
  80. package/dist/RelativeTime/RelativeTime.svelte +1 -1
  81. package/dist/SankeyChart/SankeyChart.svelte +272 -50
  82. package/dist/SankeyChart/properties.d.ts +9 -0
  83. package/dist/Scroller/Scroller.svelte +37 -2
  84. package/dist/Select/Select.svelte +166 -46
  85. package/dist/Select/properties.d.ts +7 -0
  86. package/dist/Separator/Separator.svelte +48 -0
  87. package/dist/Separator/Separator.svelte.d.ts +3 -0
  88. package/dist/Separator/properties.d.ts +22 -0
  89. package/dist/Separator/properties.js +1 -0
  90. package/dist/Sheet/Sheet.svelte +60 -21
  91. package/dist/Shimmer/Shimmer.svelte +15 -0
  92. package/dist/Slider/Slider.svelte +87 -6
  93. package/dist/Slider/properties.d.ts +26 -0
  94. package/dist/Snippet/Snippet.svelte +4 -0
  95. package/dist/Snippet/copyState.svelte.d.ts +13 -1
  96. package/dist/Snippet/copyState.svelte.js +27 -1
  97. package/dist/Snippet/properties.d.ts +7 -0
  98. package/dist/SoundKit/SoundKit.js +9 -8
  99. package/dist/SplitInput/SplitInput.svelte +68 -2
  100. package/dist/SplitInput/properties.d.ts +23 -0
  101. package/dist/StatCard/StatCard.svelte +6 -6
  102. package/dist/Status/Status.svelte +2 -2
  103. package/dist/Stepper/Step.svelte +19 -6
  104. package/dist/Stepper/Stepper.svelte +108 -35
  105. package/dist/Table/BuiltinCell.svelte +2 -2
  106. package/dist/Table/Table.svelte +373 -55
  107. package/dist/Table/properties.d.ts +134 -3
  108. package/dist/Tabs/Tabs.svelte +263 -65
  109. package/dist/Tabs/properties.d.ts +23 -0
  110. package/dist/TaskList/TaskList.svelte +14 -5
  111. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
  112. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
  113. package/dist/Toast/Toast.svelte +27 -2
  114. package/dist/Toggle/Toggle.svelte +79 -7
  115. package/dist/Toggle/Toggle.svelte.d.ts +1 -1
  116. package/dist/Toggle/properties.d.ts +30 -7
  117. package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
  118. package/dist/ToolCallLog/properties.d.ts +10 -0
  119. package/dist/Toolbar/Toolbar.svelte +1 -1
  120. package/dist/Tooltip/Tooltip.svelte +20 -2
  121. package/dist/Tooltip/tooltip-action.js +39 -4
  122. package/dist/TypewriterText/TypewriterText.svelte +34 -2
  123. package/dist/TypewriterText/properties.d.ts +18 -5
  124. package/dist/_chart/ChartContainer.svelte +12 -0
  125. package/dist/_chart/ChartTooltip.svelte +18 -4
  126. package/dist/_chart/Legend.svelte +21 -0
  127. package/dist/_chart/Legend.svelte.d.ts +15 -0
  128. package/dist/_chart/geometry.d.ts +54 -1
  129. package/dist/_chart/geometry.js +122 -14
  130. package/dist/_chart/interactions.d.ts +5 -1
  131. package/dist/_chart/interactions.js +9 -5
  132. package/dist/_chart/scales.js +16 -0
  133. package/dist/_chart/temporal.d.ts +45 -0
  134. package/dist/_chart/temporal.js +116 -0
  135. package/dist/_field/description.d.ts +50 -0
  136. package/dist/_field/description.js +51 -0
  137. package/dist/_interaction/dismissal.d.ts +67 -0
  138. package/dist/_interaction/dismissal.js +137 -0
  139. package/dist/_interaction/focus.d.ts +73 -0
  140. package/dist/_interaction/focus.js +143 -0
  141. package/dist/assets/star.svg +5 -0
  142. package/dist/index.d.ts +13 -1
  143. package/dist/index.js +21 -1
  144. package/dist/reduced-motion.svelte.d.ts +37 -0
  145. package/dist/reduced-motion.svelte.js +44 -0
  146. package/dist/styles/theme-dark.css +778 -0
  147. package/dist/types.d.ts +4 -2
  148. package/dist/utils.d.ts +62 -1
  149. package/dist/utils.js +80 -21
  150. package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
  151. package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
  152. package/dist-codemod/codemod/migrate-audit.js +395 -0
  153. package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
  154. package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
  155. package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
  156. package/dist-codemod/migrate/analyze.js +804 -0
  157. package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
  158. package/dist-wc/index.js +13992 -4323
  159. package/package.json +13 -6
  160. package/dist/Modal/focus-trap.d.ts +0 -42
  161. package/dist/Modal/focus-trap.js +0 -57
  162. /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
  163. /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
  164. /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
@@ -38,6 +38,28 @@
38
38
  id
39
39
  }: ListItemProperties = $props();
40
40
 
41
+ // DESIGN_PRINCIPLES.md principle 4: a region only takes role="button" + a tab stop when
42
+ // it was actually given its own click handler -- otherwise a single list item nests up to
43
+ // four buttons inside a button and puts five tab stops where the consumer wired one
44
+ // handler. Each of the four sub-regions is checked against its own handler prop here. The
45
+ // root is intentionally NOT re-gated the same way: its role/tabindex stay governed by the
46
+ // pre-existing `itemRole`/`suppressRoleAndTabindex` contract (see below), unconditional on
47
+ // onitemclick, matching the library's documented default ("ListItem renders synthetic
48
+ // button roles and tab stops by default").
49
+ let topSectionInteractive = $derived(
50
+ !suppressRoleAndTabindex && typeof ontopsectionclick === 'function'
51
+ );
52
+ let leftImageInteractive = $derived(
53
+ !suppressRoleAndTabindex && typeof onleftimageclick === 'function'
54
+ );
55
+ let centerTextInteractive = $derived(
56
+ !suppressRoleAndTabindex && typeof oncentertextclick === 'function'
57
+ );
58
+ let rightImageInteractive = $derived(
59
+ !suppressRoleAndTabindex && typeof onrightimageclick === 'function'
60
+ );
61
+ let itemInteractive = $derived(!suppressRoleAndTabindex && itemRole !== 'option');
62
+
41
63
  function handleLeftImageClick(event: MouseEvent): void {
42
64
  onleftimageclick?.(event);
43
65
  }
@@ -57,6 +79,49 @@
57
79
  function handleTopSectionClick(event: MouseEvent): void {
58
80
  ontopsectionclick?.(event);
59
81
  }
82
+
83
+ // A region with role="button" needs Enter/Space to do what a click does (divs get no
84
+ // free keyboard activation the way a native <button> would). Re-dispatching a real click
85
+ // on the element itself -- rather than calling its handler directly -- keeps one
86
+ // activation path and lets that click bubble through nested zones exactly like a pointer
87
+ // click already does. The `event.target === event.currentTarget` guard matters only
88
+ // because zones can nest (a consumer-supplied root handler alongside a sub-region
89
+ // handler): without it, the same keydown bubbling from a focused, activated sub-region up
90
+ // through an also-interactive ancestor would synthesize a second click at every ancestor
91
+ // it passes through.
92
+ function activateOnEnterOrSpace(interactive: boolean, event: KeyboardEvent): void {
93
+ onkeydown?.(event);
94
+ if (!interactive || event.target !== event.currentTarget) {
95
+ return;
96
+ }
97
+ if (event.key !== 'Enter' && event.key !== ' ') {
98
+ return;
99
+ }
100
+ event.preventDefault();
101
+ if (event.currentTarget instanceof HTMLElement) {
102
+ event.currentTarget.click();
103
+ }
104
+ }
105
+
106
+ function handleItemKeydown(event: KeyboardEvent): void {
107
+ activateOnEnterOrSpace(itemInteractive, event);
108
+ }
109
+
110
+ function handleTopSectionKeydown(event: KeyboardEvent): void {
111
+ activateOnEnterOrSpace(topSectionInteractive, event);
112
+ }
113
+
114
+ function handleLeftImageKeydown(event: KeyboardEvent): void {
115
+ activateOnEnterOrSpace(leftImageInteractive, event);
116
+ }
117
+
118
+ function handleCenterTextKeydown(event: KeyboardEvent): void {
119
+ activateOnEnterOrSpace(centerTextInteractive, event);
120
+ }
121
+
122
+ function handleRightImageKeydown(event: KeyboardEvent): void {
123
+ activateOnEnterOrSpace(rightImageInteractive, event);
124
+ }
60
125
  </script>
61
126
 
62
127
  {#if (typeof leftImageUrl === 'string' && leftImageUrl.length > 0) || (typeof rightImageUrl === 'string' && rightImageUrl.length > 0) || (typeof label === 'string' && label.length > 0) || typeof leftContent === 'function' || typeof centerContent === 'function' || typeof rightContent === 'function' || typeof bottomContent === 'function'}
@@ -69,7 +134,7 @@
69
134
  class="item"
70
135
  class:prevent-focus={preventFocus}
71
136
  onclick={handleItemClick}
72
- {onkeydown}
137
+ onkeydown={handleItemKeydown}
73
138
  role={suppressRoleAndTabindex ? null : (itemRole ?? 'button')}
74
139
  tabindex={suppressRoleAndTabindex ? null : itemRole === 'option' ? -1 : 0}
75
140
  aria-selected={suppressRoleAndTabindex ? null : ariaSelected}
@@ -81,9 +146,9 @@
81
146
  class="top-section"
82
147
  class:prevent-focus={preventFocus}
83
148
  onclick={handleTopSectionClick}
84
- {onkeydown}
85
- role={suppressRoleAndTabindex ? null : 'button'}
86
- tabindex={suppressRoleAndTabindex ? null : 0}
149
+ onkeydown={handleTopSectionKeydown}
150
+ role={topSectionInteractive ? 'button' : null}
151
+ tabindex={topSectionInteractive ? 0 : null}
87
152
  data-pw={topSectionTestId}
88
153
  testID={topSectionTestId}
89
154
  >
@@ -92,9 +157,9 @@
92
157
  <div
93
158
  class:prevent-focus={preventFocus}
94
159
  onclick={handleLeftImageClick}
95
- {onkeydown}
96
- role={suppressRoleAndTabindex ? null : 'button'}
97
- tabindex={suppressRoleAndTabindex ? null : 0}
160
+ onkeydown={handleLeftImageKeydown}
161
+ role={leftImageInteractive ? 'button' : null}
162
+ tabindex={leftImageInteractive ? 0 : null}
98
163
  data-pw={leftImageTestId}
99
164
  testID={leftImageTestId}
100
165
  >
@@ -111,9 +176,9 @@
111
176
  class="center-text"
112
177
  class:prevent-focus={preventFocus}
113
178
  onclick={handleCenterTextClick}
114
- {onkeydown}
115
- role={suppressRoleAndTabindex ? null : 'button'}
116
- tabindex={suppressRoleAndTabindex ? null : 0}
179
+ onkeydown={handleCenterTextKeydown}
180
+ role={centerTextInteractive ? 'button' : null}
181
+ tabindex={centerTextInteractive ? 0 : null}
117
182
  data-pw={centerTextTestId}
118
183
  testID={centerTextTestId}
119
184
  >
@@ -133,9 +198,9 @@
133
198
  <div
134
199
  class:prevent-focus={preventFocus}
135
200
  onclick={handleRightImageClick}
136
- {onkeydown}
137
- role={suppressRoleAndTabindex ? null : 'button'}
138
- tabindex={suppressRoleAndTabindex ? null : 0}
201
+ onkeydown={handleRightImageKeydown}
202
+ role={rightImageInteractive ? 'button' : null}
203
+ tabindex={rightImageInteractive ? 0 : null}
139
204
  data-pw={rightImageTestId}
140
205
  testID={rightImageTestId}
141
206
  >
@@ -192,6 +257,29 @@
192
257
  }
193
258
  }
194
259
 
260
+ /* Same hazard as Button's progress-bar loader (see its comment for the full
261
+ reasoning): this bar's width is a real elapsed-time signal over
262
+ `--list-item-loader-duration`, not decoration, so a bare `animation: none`
263
+ would freeze it at the base rule's `width: 100%` and read as "finished"
264
+ for the entire duration instead of "still loading". Discrete steps keep
265
+ the width an honest (if coarser) lower bound on elapsed time -- it can
266
+ only under-report, never claim to be further along than it is -- while
267
+ dropping the continuous slide prefers-reduced-motion targets. Lives in
268
+ the component's own <style> because that is the only stylesheet that
269
+ reaches inside the shadow root a custom-element consumer gets. */
270
+ @media (prefers-reduced-motion: reduce) {
271
+ .item-loader {
272
+ /* A LITERAL step count, deliberately not a token. `steps()` is invalid
273
+ below 1, and an invalid value does not degrade to a safer stepped
274
+ default: the whole declaration is dropped and the property falls back to
275
+ the base rule's easing, restoring the continuous slide this block exists
276
+ to remove. A consumer passing 0 to "just freeze it" would silently
277
+ switch the guard off for every user who asked for reduced motion, and a
278
+ guard that fails OPEN without saying so is worse than no guard. */
279
+ animation-timing-function: steps(5, jump-end);
280
+ }
281
+ }
282
+
195
283
  .item {
196
284
  display: flex;
197
285
  flex-direction: column;
@@ -256,7 +344,7 @@
256
344
  margin: var(--list-item-center-text-margin);
257
345
  border: var(--list-item-center-text-border);
258
346
  cursor: var(--list-item-center-text-cursor, pointer);
259
- font-family: var(--list-item-center-text-font-family);
347
+ font-family: var(--list-item-center-text-font-family, inherit);
260
348
  }
261
349
 
262
350
  .center-content {
@@ -309,11 +397,29 @@
309
397
  margin: var(--list-item-right-content-text-margin, 0px);
310
398
  border: var(--list-item-right-content-text-border);
311
399
  cursor: var(--list-item-right-content-text-cursor, pointer);
312
- font-family: var(--list-item-right-content-text-font-family);
400
+ font-family: var(--list-item-right-content-text-font-family, inherit);
313
401
  justify-content: var(--list-item-right-content-text-justify-content);
314
402
  }
315
403
 
316
404
  .prevent-focus:focus {
317
405
  outline: none;
318
406
  }
407
+ /* The loader is not the only motion in this file, and this block sits at the
408
+ END on purpose: `.item` is declared after the guard above, so an override
409
+ placed there would lose the same-specificity tie on source order -- the
410
+ cascade trap BrandLoader's own guard had to account for.
411
+ `--image-transition` is not a transition on this element at all; it is a
412
+ custom property handed DOWN to the Img child, and a custom property is the
413
+ only thing that crosses that boundary. Setting `transition: none` here would
414
+ never reach the image, so the property itself is neutralised. */
415
+ @media (prefers-reduced-motion: reduce) {
416
+ .item {
417
+ transition: none;
418
+ }
419
+
420
+ .left-content,
421
+ .right-img-wrapper {
422
+ --image-transition: none;
423
+ }
424
+ }
319
425
  </style>
@@ -44,8 +44,13 @@
44
44
  var(--loader-foreground-end) 42%
45
45
  );
46
46
  position: relative;
47
- -webkit-animation: load3 1.4s infinite linear;
48
- animation: load3 1.4s infinite linear;
47
+ /* --loader-spin-* rather than --loader-animation-* — GridItem already reads an
48
+ unrelated --loader-animation-duration for its own clipper animation (a
49
+ pre-existing, differently-scoped name this file must not collide with). */
50
+ -webkit-animation: load3 var(--loader-spin-duration, var(--motion-duration, 1.4s))
51
+ var(--loader-spin-easing, var(--motion-easing, linear)) infinite;
52
+ animation: load3 var(--loader-spin-duration, var(--motion-duration, 1.4s))
53
+ var(--loader-spin-easing, var(--motion-easing, linear)) infinite;
49
54
  -webkit-transform: translateZ(0);
50
55
  -ms-transform: translateZ(0);
51
56
  transform: translateZ(0);
@@ -91,4 +96,19 @@
91
96
  transform: rotate(360deg);
92
97
  }
93
98
  }
99
+ /* An indefinite animation is the case this preference exists for: it never
100
+ ends, so a user who asked the OS to minimise motion gets a permanent loop.
101
+ The animation stops, but the element must still read as "busy" -- a guard
102
+ that leaves nothing on screen, or leaves a frame that means something else,
103
+ is worse than the motion it removed. This block lives in the component's own
104
+ <style> because that is the only stylesheet that reaches inside the shadow
105
+ root a custom-element consumer gets. */
106
+ @media (prefers-reduced-motion: reduce) {
107
+ .loader,
108
+ .loader:before,
109
+ .loader:after {
110
+ -webkit-animation: none;
111
+ animation: none;
112
+ }
113
+ }
94
114
  </style>
@@ -70,4 +70,25 @@
70
70
  transform: scale(1);
71
71
  }
72
72
  }
73
+ /* An indefinite animation is the case this preference exists for: it never
74
+ ends, so a user who asked the OS to minimise motion gets a permanent loop.
75
+ The animation stops, but the element must still read as "busy" -- a guard
76
+ that leaves nothing on screen, or leaves a frame that means something else,
77
+ is worse than the motion it removed. This block lives in the component's own
78
+ <style> because that is the only stylesheet that reaches inside the shadow
79
+ root a custom-element consumer gets. */
80
+ @media (prefers-reduced-motion: reduce) {
81
+ /* `.dot.pulse` further down sets `animation-name`, and it is both more
82
+ specific and later, so a lone `.dot` rule here loses to it and the pulse
83
+ variant keeps animating. Matching its specificity is what makes the guard
84
+ apply to both variants. */
85
+ .dot,
86
+ .dot.pulse {
87
+ /* The rest frame of both keyframe sets is already the visible one --
88
+ translateY(0) at full opacity -- so stopping leaves the three dots
89
+ legible rather than collapsed or invisible. */
90
+ animation: none;
91
+ transform: none;
92
+ }
93
+ }
73
94
  </style>
@@ -322,7 +322,7 @@
322
322
  .counter {
323
323
  font-size: var(--media-upload-counter-font-size, 12px);
324
324
  font-weight: var(--media-upload-counter-font-weight, 500);
325
- color: var(--media-upload-counter-color, #8a8f98);
325
+ color: var(--media-upload-counter-color, #5a5a5a);
326
326
  font-variant-numeric: tabular-nums;
327
327
  }
328
328
 
@@ -475,7 +475,7 @@
475
475
  text-align: center;
476
476
  background-color: var(--media-upload-add-background-color, #fafafa);
477
477
  border: var(--media-upload-add-border, 1.5px dashed #c8ccd2);
478
- color: var(--media-upload-add-color, #6b7280);
478
+ color: var(--media-upload-add-color, #4b5563);
479
479
  cursor: pointer;
480
480
  transition: var(
481
481
  --media-upload-add-transition,
@@ -517,7 +517,7 @@
517
517
 
518
518
  .drop-hint {
519
519
  font-size: var(--media-upload-add-hint-font-size, 10px);
520
- color: var(--media-upload-add-hint-color, #9aa0a8);
520
+ color: var(--media-upload-add-hint-color, #4b5563);
521
521
  }
522
522
 
523
523
  .error {
@@ -525,4 +525,14 @@
525
525
  color: var(--media-upload-error-color, #e0334b);
526
526
  margin: var(--media-upload-error-margin, 8px 0 0 0);
527
527
  }
528
+
529
+ /* `--media-upload-item-transition` defaults to a box-shadow AND transform lift
530
+ on hover. The shadow alone would be fine; the transform is movement, and a
531
+ consumer overriding the token is most likely adding more of it. Dropping the
532
+ whole declaration is right here because every end state is a resting state. */
533
+ @media (prefers-reduced-motion: reduce) {
534
+ .media-item {
535
+ transition: none;
536
+ }
537
+ }
528
538
  </style>
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
- import { tick, onMount } from 'svelte';
2
+ import { tick, onDestroy } from 'svelte';
3
3
  import { SvelteMap } from 'svelte/reactivity';
4
4
  import Img from '../Img/Img.svelte';
5
5
  import { computeMenuDropdownPosition } from './dropdownPosition';
6
+ import { eventHitsInside, registerDismissible } from '../_interaction/dismissal';
6
7
  import type { MenuProperties, MenuItem, MenuPlacement } from './properties';
7
8
 
8
9
  let {
@@ -110,15 +111,30 @@
110
111
  });
111
112
 
112
113
  /**
113
- * Svelte action: relocates the dropdown to document.body when usePortal is set,
114
+ * Where a portalled node is allowed to land. Svelte scopes a custom element's
115
+ * CSS to its shadow root, so `document.body` is the one place the panel must
116
+ * never go from inside `<sui-menu>`: it keeps its `svelte-*` scoping class and
117
+ * loses every rule behind it, plus the host's custom properties. Measured in
118
+ * Chromium -- position fell back to `static`, background to transparent,
119
+ * border and shadow to none, so the items rendered as bare text over the page.
120
+ * Its own root holds that stylesheet and still sits above every overflow/
121
+ * scroll ancestor between the two.
122
+ */
123
+ const portalTarget = (node: Node): Node => {
124
+ const root = node.getRootNode();
125
+ return root instanceof ShadowRoot ? root : document.body;
126
+ };
127
+
128
+ /**
129
+ * Svelte action: relocates the dropdown out to its root when usePortal is set,
114
130
  * so a position:fixed panel is never clipped by an overflow/scroll ancestor
115
131
  * (e.g. a table cell). No-op otherwise; `use:` actions never run during SSR.
116
132
  */
117
- const portalToBody = (node: HTMLElement) => {
133
+ const portalToRoot = (node: HTMLElement) => {
118
134
  if (!usePortal) {
119
135
  return;
120
136
  }
121
- document.body.appendChild(node);
137
+ portalTarget(node).appendChild(node);
122
138
  return { destroy: () => node.remove() };
123
139
  };
124
140
 
@@ -315,11 +331,6 @@
315
331
  }
316
332
  break;
317
333
  }
318
- case 'Escape': {
319
- event.preventDefault();
320
- close();
321
- break;
322
- }
323
334
  case 'Tab': {
324
335
  close();
325
336
  break;
@@ -352,29 +363,45 @@
352
363
  }
353
364
  }
354
365
 
355
- function handleClickOutside(event: Event) {
356
- // A portaled panel lives outside menuContainerEl, so a click on a separator
366
+ function handleOutsidePress(event: Event) {
367
+ // A portaled panel lives outside menuContainerEl, so a press on a separator
357
368
  // or padding inside it is not contained — treat the panel node as "inside"
358
- // too, matching the in-flow behaviour of not closing on such clicks.
359
- if (
360
- open &&
361
- event.target instanceof Node &&
362
- menuContainerEl !== null &&
363
- !menuContainerEl.contains(event.target) &&
364
- !(menuListEl !== null && menuListEl.contains(event.target))
365
- ) {
366
- close();
369
+ // too, matching the in-flow behaviour of not closing on such presses. The
370
+ // module has already established the press is outside menuContainerEl
371
+ // (that's `element` below) before calling this at all, so only the portal
372
+ // case needs a second check here.
373
+ if (eventHitsInside(menuListEl, event)) {
374
+ return;
367
375
  }
376
+ close();
368
377
  }
369
378
 
370
- onMount(() => {
371
- document.addEventListener('click', handleClickOutside);
372
- return () => {
373
- document.removeEventListener('click', handleClickOutside);
374
- if (typeaheadTimer !== null) {
375
- clearTimeout(typeaheadTimer);
376
- }
379
+ /**
380
+ * Registers this dropdown as the topmost dismissible layer for exactly the
381
+ * span it exists (the `{#if open}` block below) and releases it again on
382
+ * teardown — including when the component unmounts while still open, so a
383
+ * stale layer never survives to swallow a later Escape meant for whatever
384
+ * opens next. Escape and outside-press used to be answered here directly
385
+ * (a document click listener added in onMount, and an Escape case in
386
+ * handleMenuKeydown); routed through the shared module instead so only the
387
+ * topmost open surface — this menu, or a Modal/Sheet/another Menu opened on
388
+ * top of it — answers either.
389
+ */
390
+ function dismissalAction(_node: HTMLDivElement) {
391
+ const release = registerDismissible({
392
+ element: () => menuContainerEl,
393
+ onEscape: close,
394
+ onOutside: handleOutsidePress
395
+ });
396
+ return {
397
+ destroy: release
377
398
  };
399
+ }
400
+
401
+ onDestroy(() => {
402
+ if (typeaheadTimer !== null) {
403
+ clearTimeout(typeaheadTimer);
404
+ }
378
405
  });
379
406
  </script>
380
407
 
@@ -438,7 +465,8 @@
438
465
  tabindex="-1"
439
466
  style={portalStyle}
440
467
  onkeydown={handleMenuKeydown}
441
- use:portalToBody
468
+ use:portalToRoot
469
+ use:dismissalAction
442
470
  >
443
471
  {#each items as item (item.value)}
444
472
  {#if item.separator === true}
@@ -4,7 +4,8 @@
4
4
  import ModalAnimation from '../Animations/ModalAnimation.svelte';
5
5
  import OverlayAnimation from '../Animations/OverlayAnimation.svelte';
6
6
  import { createDebouncer, lockBodyScroll, unlockBodyScroll } from '../utils';
7
- import { focusEntryPoint, focusTrapTabTarget } from './focus-trap';
7
+ import { focusEntryPoint, focusTrapTabTarget, getActiveElement } from '../_interaction/focus';
8
+ import { registerDismissible } from '../_interaction/dismissal';
8
9
  import Button from '../Button/Button.svelte';
9
10
  import Img from '../Img/Img.svelte';
10
11
 
@@ -68,17 +69,35 @@
68
69
  // resetting the timer and breaking debounce correctness.
69
70
  const debounce = createDebouncer(debounceTime);
70
71
 
72
+ /**
73
+ * Where a portalled node is allowed to land. Svelte scopes a custom element's
74
+ * CSS to its shadow root, so `document.body` is the one place the overlay must
75
+ * never go from inside `<sui-modal>`: it keeps its `svelte-*` scoping class and
76
+ * loses every rule behind it, plus the host's custom properties. Measured in
77
+ * Chromium -- position fell back to `static`, background to transparent, so the
78
+ * dialog stopped being a dialog and became a block of text in the page flow.
79
+ * Its own root holds that stylesheet and still escapes every clipping and
80
+ * stacking ancestor between the two.
81
+ */
82
+ const portalTarget = (node: Node): Node => {
83
+ const root = node.getRootNode();
84
+ return root instanceof ShadowRoot ? root : document.body;
85
+ };
86
+
71
87
  // Fix [minor]: portalAction with null guard + update hook so usePortal toggles work post-mount.
72
88
  const portalAction = (node: HTMLElement, params: { usePortal: boolean }) => {
73
89
  if (!params.usePortal || typeof document === 'undefined' || !document.body) {
74
90
  return;
75
91
  }
76
- const target = document.body;
92
+ // Resolved once, while the node is still in the tree it was rendered into --
93
+ // after the move its own root is the destination, so re-reading it would only
94
+ // ever answer with wherever it already is.
95
+ const target = portalTarget(node);
77
96
  target.appendChild(node);
78
97
  return {
79
98
  update(updatedParams: { usePortal: boolean }) {
80
- if (updatedParams.usePortal && !node.parentElement?.isSameNode(document.body)) {
81
- document.body.appendChild(node);
99
+ if (updatedParams.usePortal && node.parentNode !== target) {
100
+ target.appendChild(node);
82
101
  }
83
102
  },
84
103
  destroy() {
@@ -87,23 +106,6 @@
87
106
  };
88
107
  };
89
108
 
90
- // Resolves the real focused element regardless of whether modalContent sits
91
- // in the light DOM or inside Modal.wc.svelte's open shadow root.
92
- // document.activeElement stops at a shadow boundary and returns the shadow
93
- // host itself, never the element actually focused inside it -- getRootNode()
94
- // returns that shadow root directly when modalContent is shadow-hosted, and
95
- // the owner Document otherwise.
96
- const getActiveElement = (): Element | null => {
97
- if (modalContent === null) {
98
- return null;
99
- }
100
- const root = modalContent.getRootNode();
101
- if (root instanceof Document || root instanceof ShadowRoot) {
102
- return root.activeElement;
103
- }
104
- return null;
105
- };
106
-
107
109
  const handlePopstate = (): void => {
108
110
  backPressed = true;
109
111
  onclose?.();
@@ -135,18 +137,17 @@
135
137
  }
136
138
  };
137
139
 
140
+ // Escape itself is no longer decided here -- see handleEscape and the
141
+ // registerDismissible call in onMount below. This still runs on every
142
+ // keydown (Escape included) purely to forward the onkeydown prop and to
143
+ // run the Tab trap; it must never re-decide who Escape belongs to.
138
144
  const handleKeyDown = (event: KeyboardEvent): void => {
139
145
  onkeydown?.(event);
140
146
  const key = event?.key;
141
- if (key === 'Escape') {
142
- onoverlayclick?.();
143
- ondismiss?.();
144
- return;
145
- }
146
147
  if (key === 'Tab') {
147
148
  const target = focusTrapTabTarget({
148
149
  container: modalContent,
149
- activeElement: getActiveElement(),
150
+ activeElement: getActiveElement(modalContent),
150
151
  shiftKey: event.shiftKey
151
152
  });
152
153
  if (target !== null) {
@@ -156,14 +157,24 @@
156
157
  }
157
158
  };
158
159
 
160
+ // What the shared dismissal module calls when this modal is the topmost
161
+ // dismissible layer and Escape is pressed -- same callbacks the old
162
+ // Escape branch in handleKeyDown used to invoke directly, just gated on
163
+ // ownership now instead of firing for every open modal unconditionally.
164
+ const handleEscape = (): void => {
165
+ onoverlayclick?.();
166
+ ondismiss?.();
167
+ };
168
+
159
169
  // The overlay's own Enter/Space activation, additional to the shared
160
- // handleKeyDown (Escape/Tab/onkeydown-prop) that svelte:window already
161
- // delivers here via bubbling -- this handler must not also call
162
- // handleKeyDown itself, or every keydown on/under the overlay would run it
163
- // twice (once from this direct call, once from the window listener seeing
164
- // the same event bubble past it), double-firing onkeydown and, on Escape,
165
- // the dismiss callbacks. Enter/Space activation is scoped to this handler,
166
- // not folded into handleKeyDown itself, because handleKeyDown also runs
170
+ // handleKeyDown (Tab/onkeydown-prop -- Escape is the dismissal module's,
171
+ // via handleEscape) that svelte:window already delivers here via
172
+ // bubbling -- this handler must not also call handleKeyDown itself, or
173
+ // every keydown on/under the overlay would run it twice (once from this
174
+ // direct call, once from the window listener seeing the same event bubble
175
+ // past it), double-firing onkeydown and re-running the Tab trap. Enter/Space
176
+ // activation is scoped to this handler, not folded into handleKeyDown itself,
177
+ // because handleKeyDown also runs
167
178
  // from the window listener for a keypress anywhere in the modal (e.g. an
168
179
  // input field) -- it must not treat every Enter/Space in the modal as an
169
180
  // overlay dismissal. This handler is bound to the overlay div, so a keydown
@@ -202,9 +213,30 @@
202
213
  }
203
214
  };
204
215
 
216
+ // What THIS instance acquired, rather than what the prop says at teardown. The
217
+ // lock is reference counted and shared with Sheet and CommandMenu, so reading
218
+ // `lockScroll` again in onDestroy releases the wrong number of holds whenever the
219
+ // prop changed while the modal was open: turned off, the count never comes back
220
+ // down and the page stays frozen; turned on, this modal releases a hold another
221
+ // component is still relying on. Release exactly what was taken.
222
+ let heldScrollLock = false;
223
+
224
+ // Set once in onMount, cleared once in onDestroy -- registerDismissible's
225
+ // own release function already tolerates a repeat call, but this still
226
+ // guards against calling it twice from here (see the "release exactly
227
+ // once" comment on unlockBodyScroll's own bookkeeping above for why that
228
+ // matters generally: a stale layer left registered after unmount would
229
+ // swallow every later Escape in the app, and would never come back).
230
+ let releaseDismissible: (() => void) | null = null;
231
+
205
232
  onMount(() => {
233
+ releaseDismissible = registerDismissible({
234
+ element: () => modalContent,
235
+ onEscape: handleEscape
236
+ });
206
237
  if (lockScroll) {
207
238
  lockBodyScroll();
239
+ heldScrollLock = true;
208
240
  }
209
241
  if (typeof autoDismissAfter === 'number') {
210
242
  dismissTimer = setTimeout(() => onclose?.(), autoDismissAfter);
@@ -221,19 +253,22 @@
221
253
  // moving focus in, so onDestroy can put it back. Shadow-aware for the
222
254
  // same reason the Tab trap is: inside Modal.wc.svelte's open shadow
223
255
  // root, document.activeElement would return the shadow host instead.
224
- const active = getActiveElement();
256
+ const active = getActiveElement(modalContent);
225
257
  previouslyFocusedElement = active instanceof HTMLElement ? active : null;
226
258
  focusEntryPoint(modalContent);
227
259
  });
228
260
  });
229
261
 
230
262
  onDestroy(() => {
263
+ releaseDismissible?.();
264
+ releaseDismissible = null;
231
265
  if (dismissTimer !== null) {
232
266
  clearTimeout(dismissTimer);
233
267
  }
234
268
  if (typeof window !== 'undefined') {
235
- if (lockScroll) {
269
+ if (heldScrollLock) {
236
270
  unlockBodyScroll();
271
+ heldScrollLock = false;
237
272
  }
238
273
  if (supportHardwareBackPress) {
239
274
  if (!backPressed) {
@@ -411,9 +446,9 @@
411
446
  overflow: var(--modal-content-overflow, auto);
412
447
  border-top: var(--modal-content-border-top);
413
448
  /* tabindex="-1" makes this the focus-trap's fallback target when the modal
414
- has no focusable content of its own (see focus-trap.ts); it is never in
415
- the Tab order itself, so it needs no visible focus ring the way a real
416
- focusable element would. Matches Sheet's sheet-panel. */
449
+ has no focusable content of its own (see _interaction/focus.ts); it is
450
+ never in the Tab order itself, so it needs no visible focus ring the way
451
+ a real focusable element would. Matches Sheet's sheet-panel. */
417
452
  outline: none;
418
453
  /* Viewport containment for every size class: only .fit-content used to carry
419
454
  a max-height, so a size whose height var is overridden to fit-content (or
@@ -584,9 +619,9 @@
584
619
  align-items: center;
585
620
  flex: 1;
586
621
  font-size: var(--header-text-size, 16px);
587
- font-weight: var(--modal-header-text-weight);
588
- line-height: var(--modal-header-text-line-height);
589
- letter-spacing: var(--modal-header-text-letter-spacing);
622
+ font-weight: var(--modal-header-text-weight, inherit);
623
+ line-height: var(--modal-header-text-line-height, inherit);
624
+ letter-spacing: var(--modal-header-text-letter-spacing, inherit);
590
625
  }
591
626
 
592
627
  /* The header images render through the Img component (inline svg or img), so