@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
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { describeField } from '../_field/description';
2
3
  import type { SliderProperties } from './properties';
3
4
 
4
5
  let {
@@ -14,11 +15,47 @@
14
15
  testId,
15
16
  onchange,
16
17
  oninput,
17
- classes
18
+ classes,
19
+ ariaValueText,
20
+ name,
21
+ form,
22
+ errorMessage,
23
+ infoMessage,
24
+ invalid = false
18
25
  }: SliderProperties = $props();
19
26
 
20
- let percentage = $derived(((value - min) / (max - min)) * 100);
27
+ /* The control and the text that explains it were never linked: a screen-reader
28
+ user reaching this control heard its name and nothing about why it was
29
+ rejected. `describeField` composes the reference from the messages that are
30
+ actually rendered, so aria-describedby never points at an id that is not in
31
+ the DOM -- which passes an attribute assertion and resolves to nothing in a
32
+ real reader. */
33
+ const fieldUid = $props.id();
34
+ const field = $derived(
35
+ describeField(fieldUid, { error: errorMessage, info: infoMessage, invalid })
36
+ );
37
+
38
+ // Only the paint is clamped. The value stays exactly what the consumer passed, so a
39
+ // slider handed a number outside its own bounds reports that number rather than
40
+ // being silently corrected — but the track never renders a negative, >100% or NaN
41
+ // fill. An unusable range (empty, reversed, non-finite, or one whose width or
42
+ // quotient overflows to Infinity) has no meaningful fill, so it paints none.
43
+ const percentage: number = $derived.by(() => {
44
+ const span = max - min;
45
+ if (![value, min, max, span].every((part) => Number.isFinite(part)) || span <= 0) {
46
+ return 0;
47
+ }
48
+ const raw = ((value - min) / span) * 100;
49
+ return Number.isFinite(raw) ? Math.min(100, Math.max(0, raw)) : 0;
50
+ });
21
51
  let displayValue = $derived(labelFormatter ? labelFormatter(value) : String(value));
52
+ const valueText: string | null = $derived(
53
+ typeof ariaValueText === 'string'
54
+ ? ariaValueText
55
+ : labelFormatter
56
+ ? labelFormatter(value)
57
+ : null
58
+ );
22
59
 
23
60
  function handleInput(e: Event) {
24
61
  if (e.target instanceof HTMLInputElement) {
@@ -38,12 +75,18 @@
38
75
  <div class="slider-container {classes ?? ''}" class:disabled>
39
76
  <input
40
77
  type="range"
78
+ aria-describedby={field.describedBy}
79
+ aria-invalid={field.ariaInvalid}
41
80
  class="slider-input"
42
81
  {min}
43
82
  {max}
44
83
  {step}
45
84
  {value}
46
85
  {disabled}
86
+ {name}
87
+ form={typeof form === 'string' ? form : null}
88
+ aria-valuetext={valueText}
89
+ data-disabled={disabled ? '' : null}
47
90
  aria-label={typeof ariaLabel === 'string' ? ariaLabel : null}
48
91
  aria-labelledby={typeof ariaLabel === 'string' ? null : (ariaLabelledby ?? null)}
49
92
  data-pw={typeof testId === 'string' ? testId : null}
@@ -57,6 +100,28 @@
57
100
  {/if}
58
101
  </div>
59
102
 
103
+ {#if field.showsError}
104
+ <div
105
+ id={field.errorId}
106
+ role="alert"
107
+ class="field-error"
108
+ data-pw={typeof testId === 'string' ? `${testId}-error-message` : null}
109
+ testID={typeof testId === 'string' ? `${testId}-error-message` : null}
110
+ >
111
+ {errorMessage}
112
+ </div>
113
+ {/if}
114
+ {#if field.showsInfo}
115
+ <div
116
+ id={field.infoId}
117
+ class="field-info"
118
+ data-pw={typeof testId === 'string' ? `${testId}-info-message` : null}
119
+ testID={typeof testId === 'string' ? `${testId}-info-message` : null}
120
+ >
121
+ {infoMessage}
122
+ </div>
123
+ {/if}
124
+
60
125
  <style>
61
126
  .slider-container {
62
127
  display: flex;
@@ -109,8 +174,10 @@
109
174
  opacity: var(--slider-thumb-opacity, 1);
110
175
  cursor: pointer;
111
176
  transition:
112
- transform 0.15s ease,
113
- opacity 0.15s ease;
177
+ transform var(--slider-thumb-transition-duration, var(--motion-duration, 0.15s))
178
+ var(--slider-thumb-transition-easing, var(--motion-easing, ease)),
179
+ opacity var(--slider-thumb-transition-duration, var(--motion-duration, 0.15s))
180
+ var(--slider-thumb-transition-easing, var(--motion-easing, ease));
114
181
  }
115
182
 
116
183
  .slider-input::-moz-range-thumb {
@@ -123,8 +190,10 @@
123
190
  opacity: var(--slider-thumb-opacity, 1);
124
191
  cursor: pointer;
125
192
  transition:
126
- transform 0.15s ease,
127
- opacity 0.15s ease;
193
+ transform var(--slider-thumb-transition-duration, var(--motion-duration, 0.15s))
194
+ var(--slider-thumb-transition-easing, var(--motion-easing, ease)),
195
+ opacity var(--slider-thumb-transition-duration, var(--motion-duration, 0.15s))
196
+ var(--slider-thumb-transition-easing, var(--motion-easing, ease));
128
197
  }
129
198
 
130
199
  .slider-input:hover::-webkit-slider-thumb {
@@ -159,4 +228,16 @@
159
228
  color: var(--slider-value-color, #333333);
160
229
  white-space: nowrap;
161
230
  }
231
+
232
+ .field-error {
233
+ color: var(--field-error-color, #c5120a);
234
+ font-size: var(--field-error-font-size, 12px);
235
+ margin: var(--field-error-margin, 4px 0 0 0);
236
+ }
237
+
238
+ .field-info {
239
+ color: var(--field-info-color, #6b7280);
240
+ font-size: var(--field-info-font-size, 12px);
241
+ margin: var(--field-info-margin, 4px 0 0 0);
242
+ }
162
243
  </style>
@@ -3,6 +3,22 @@ export type MandatorySliderProperties = {
3
3
  value: number;
4
4
  };
5
5
  export type OptionalSliderProperties = {
6
+ /**
7
+ * Text shown, and announced, when the control is in error. Linked to the
8
+ * control through `aria-describedby`, and sets `aria-invalid` while present.
9
+ */
10
+ errorMessage?: string | null;
11
+ /**
12
+ * Persistent helper text describing the control. Linked through
13
+ * `aria-describedby` too, so it is read before the user trips an error rather
14
+ * than only after.
15
+ */
16
+ infoMessage?: string | null;
17
+ /**
18
+ * Marks the control invalid without supplying a message, for a consumer
19
+ * driving validity from a server or its own rules.
20
+ */
21
+ invalid?: boolean;
6
22
  min?: number;
7
23
  max?: number;
8
24
  step?: number;
@@ -23,6 +39,16 @@ export type OptionalSliderProperties = {
23
39
  ariaLabelledby?: string;
24
40
  testId?: string;
25
41
  classes?: string;
42
+ /** Spoken in place of the raw number, for a value whose meaning the digits do not
43
+ * carry ("Quiet", "Large"). Falls back to `labelFormatter(value)` when that is
44
+ * supplied, and is omitted entirely when neither is. An explicit empty string is
45
+ * honoured rather than treated as absent. */
46
+ ariaValueText?: string;
47
+ /** Submits the current value under this name. Omitted while disabled. */
48
+ name?: string;
49
+ /** `id` of a form elsewhere in the same document. Unavailable through
50
+ * `<sui-slider>`, whose input sits in a shadow root. */
51
+ form?: string;
26
52
  };
27
53
  export type SliderEventProperties = {
28
54
  onchange?: (value: number) => void;
@@ -14,6 +14,7 @@
14
14
  copiedLabel = 'Copied!',
15
15
  copyResetMs = 2000,
16
16
  oncopy,
17
+ onerror,
17
18
  classes
18
19
  }: SnippetProperties = $props();
19
20
 
@@ -29,6 +30,9 @@
29
30
  },
30
31
  get oncopy() {
31
32
  return oncopy;
33
+ },
34
+ get onerror() {
35
+ return onerror;
32
36
  }
33
37
  });
34
38
  onDestroy(copyState.destroy);
@@ -31,6 +31,17 @@ export type CopyStateOptions = {
31
31
  copyResetMs?: number;
32
32
  /** Called once per successful copy. Never called when the write fails. */
33
33
  oncopy?: () => void;
34
+ /**
35
+ * Called instead of `oncopy` when a write cannot complete -- a rejection
36
+ * (denied permission, a non-secure context) or a Clipboard API that is
37
+ * absent entirely (SSR, a sandboxed iframe). Receives the rejection
38
+ * reason, or an `Error` when the API itself is missing.
39
+ *
40
+ * Without it the caller sees only `copy()` resolving `false` and cannot
41
+ * distinguish "refused" from "no clipboard here", which is the difference
42
+ * between a message worth showing the user and one worth suppressing.
43
+ */
44
+ onerror?: (reason: unknown) => void;
34
45
  };
35
46
  export type CopyState = {
36
47
  /** True from a successful copy until the reset delay elapses. */
@@ -38,7 +49,8 @@ export type CopyState = {
38
49
  /**
39
50
  * Writes `text` to the clipboard. Resolves `true` on success, `false` if
40
51
  * the clipboard was unavailable or refused — it never rejects, so a caller
41
- * needs no try/catch of its own.
52
+ * needs no try/catch of its own. A `false` outcome is also reported to
53
+ * `onerror` with the reason.
42
54
  */
43
55
  copy: (text: string) => Promise<boolean>;
44
56
  /** Cancels any pending reset. Call from the owner's teardown. */
@@ -20,6 +20,20 @@ function resolveResetDelay(options) {
20
20
  }
21
21
  return requested;
22
22
  }
23
+ /**
24
+ * `onerror` is a consumer callback like `oncopy`, so it gets the same
25
+ * containment: a throwing reporter must not turn a handled failure into an
26
+ * unhandled rejection out of `copy()`, which is the one thing this factory
27
+ * promises never to do.
28
+ */
29
+ function report(options, reason) {
30
+ try {
31
+ options.onerror?.(reason);
32
+ }
33
+ catch {
34
+ /* A failed notification cannot make the clipboard write any less failed. */
35
+ }
36
+ }
23
37
  export function createCopyState(options = {}) {
24
38
  let copied = $state(false);
25
39
  let destroyed = false;
@@ -32,14 +46,26 @@ export function createCopyState(options = {}) {
32
46
  if (destroyed) {
33
47
  return false;
34
48
  }
49
+ // Checked before the call rather than relying on the catch below: with
50
+ // no `navigator.clipboard` at all, `writeText` is a property access on
51
+ // undefined, and the resulting TypeError says nothing a caller could
52
+ // act on. An explicit reason distinguishes "this context has no
53
+ // clipboard" from "the user refused". `navigator` is tested for null
54
+ // separately from `undefined`: a stubbed-out global is routinely the
55
+ // former, and `!navigator.clipboard` would throw on it.
56
+ if (typeof navigator === 'undefined' || navigator === null || !navigator.clipboard) {
57
+ report(options, new Error('Clipboard API is unavailable in this context.'));
58
+ return false;
59
+ }
35
60
  try {
36
61
  await navigator.clipboard.writeText(text);
37
62
  }
38
- catch {
63
+ catch (reason) {
39
64
  /* Clipboard unavailable: non-secure context, sandboxed iframe, or a
40
65
  permission refusal. Reporting success here — and flashing "Copied!"
41
66
  over a clipboard that never received the text — is the one outcome
42
67
  worse than showing no feedback at all. */
68
+ report(options, reason);
43
69
  return false;
44
70
  }
45
71
  // The clipboard promise can settle after its owner has unmounted.
@@ -16,4 +16,11 @@ export type OptionalSnippetProperties = {
16
16
  };
17
17
  export type SnippetEventProperties = {
18
18
  oncopy?: () => void;
19
+ /**
20
+ * Fires when a clipboard write fails -- rejected (denied permission, a
21
+ * non-secure context) or the Clipboard API being entirely absent -- with
22
+ * the rejection reason (an `Error` when the API itself is missing).
23
+ * `oncopy` does not fire for the same attempt.
24
+ */
25
+ onerror?: (reason: unknown) => void;
19
26
  };
@@ -1,3 +1,4 @@
1
+ import { closestInEventPath } from '../_interaction/dismissal';
1
2
  const DEFAULT_STORAGE_KEY = 'sui-sound-enabled';
2
3
  const DEFAULT_MASTER_GAIN = 0.32;
3
4
  const SOUND_ATTRIBUTE = 'data-sound';
@@ -216,11 +217,11 @@ export const createSoundKit = (options = {}) => {
216
217
  if (!resolveEnabled()) {
217
218
  return;
218
219
  }
219
- const target = event.target;
220
- if (!(target instanceof Element)) {
221
- return;
222
- }
223
- const overrideElement = target.closest(`[${SOUND_ATTRIBUTE}]`);
220
+ // This listener sits on `document`, so `event.target` is the shadow HOST for
221
+ // any click inside a `<sui-*>` element and `target.closest()` would search
222
+ // the light tree instead of the one that was clicked -- no sound for every
223
+ // custom-element click, or the wrong sound inherited from a light ancestor.
224
+ const overrideElement = closestInEventPath(event, `[${SOUND_ATTRIBUTE}]`);
224
225
  if (overrideElement !== null) {
225
226
  const overrideValue = overrideElement.getAttribute(SOUND_ATTRIBUTE) ?? '';
226
227
  const resolved = resolveOverride(overrideValue);
@@ -229,15 +230,15 @@ export const createSoundKit = (options = {}) => {
229
230
  }
230
231
  return;
231
232
  }
232
- if (target.closest(TICK_SELECTOR) !== null) {
233
+ if (closestInEventPath(event, TICK_SELECTOR) !== null) {
233
234
  play('tick');
234
235
  return;
235
236
  }
236
- if (target.closest(PAGE_SELECTOR) !== null) {
237
+ if (closestInEventPath(event, PAGE_SELECTOR) !== null) {
237
238
  play('page');
238
239
  return;
239
240
  }
240
- if (target.closest(PRESS_SELECTOR) !== null) {
241
+ if (closestInEventPath(event, PRESS_SELECTOR) !== null) {
241
242
  play('press');
242
243
  }
243
244
  };
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
+ import { describeField } from '../_field/description';
3
4
  import Input from '../Input/Input.svelte';
4
5
  import type { FieldConfig, SplitInputProperties } from './properties';
5
6
 
@@ -12,11 +13,34 @@
12
13
  separator,
13
14
  testId,
14
15
  classes,
16
+ ariaLabel,
17
+ errorMessage,
18
+ infoMessage,
19
+ invalid = false,
15
20
  onchange,
16
21
  oninput,
17
22
  oncomplete
18
23
  }: SplitInputProperties = $props();
19
24
 
25
+ /* "That code is not right" is a fact about the whole code, not about the box
26
+ the caret happens to be in, so the description belongs on the group -- and
27
+ there was none: a screen-reader user got four unlabelled boxes and no way to
28
+ reach the message. `describeField` composes the reference from the messages
29
+ that are actually rendered, so aria-describedby never points at an id that
30
+ is not in the DOM -- which passes an attribute assertion and resolves to
31
+ nothing in a real reader.
32
+
33
+ The role is conditional so a SplitInput with no messages and no label keeps
34
+ the accessibility tree it had before these props existed. */
35
+ const fieldUid = $props.id();
36
+ const field = $derived(
37
+ describeField(fieldUid, { error: errorMessage, info: infoMessage, invalid })
38
+ );
39
+ const hasGroupLabel = $derived(typeof ariaLabel === 'string' && ariaLabel !== '');
40
+ const describedGroup = $derived(
41
+ field.describedBy !== null || field.ariaInvalid !== null || hasGroupLabel
42
+ );
43
+
20
44
  let fieldCount = $derived(typeof fields !== 'undefined' ? fields.length : length);
21
45
 
22
46
  let fieldConfigs: FieldConfig[] = $derived(
@@ -223,7 +247,15 @@
223
247
  }
224
248
  </script>
225
249
 
226
- <div class="field-group {classes ?? ''}" data-pw={testId} testID={testId}>
250
+ <div
251
+ class="field-group {classes ?? ''}"
252
+ role={describedGroup ? 'group' : null}
253
+ aria-label={hasGroupLabel ? ariaLabel : null}
254
+ aria-describedby={field.describedBy}
255
+ aria-invalid={field.ariaInvalid}
256
+ data-pw={testId}
257
+ testID={testId}
258
+ >
227
259
  {#each fieldConfigs as config, index (index)}
228
260
  {#if typeof separator === 'string' && index > 0}
229
261
  <span class="field-group-separator">{separator}</span>
@@ -256,7 +288,41 @@
256
288
  {/each}
257
289
  </div>
258
290
 
291
+ {#if field.showsError}
292
+ <div
293
+ id={field.errorId}
294
+ role="alert"
295
+ class="field-error"
296
+ data-pw={typeof testId === 'string' ? `${testId}-error-message` : null}
297
+ testID={typeof testId === 'string' ? `${testId}-error-message` : null}
298
+ >
299
+ {errorMessage}
300
+ </div>
301
+ {/if}
302
+ {#if field.showsInfo}
303
+ <div
304
+ id={field.infoId}
305
+ class="field-info"
306
+ data-pw={typeof testId === 'string' ? `${testId}-info-message` : null}
307
+ testID={typeof testId === 'string' ? `${testId}-info-message` : null}
308
+ >
309
+ {infoMessage}
310
+ </div>
311
+ {/if}
312
+
259
313
  <style>
314
+ .field-error {
315
+ color: var(--field-error-color, #c5120a);
316
+ font-size: var(--field-error-font-size, 12px);
317
+ margin: var(--field-error-margin, 4px 0 0 0);
318
+ }
319
+
320
+ .field-info {
321
+ color: var(--field-info-color, #6b7280);
322
+ font-size: var(--field-info-font-size, 12px);
323
+ margin: var(--field-info-margin, 4px 0 0 0);
324
+ }
325
+
260
326
  .field-group {
261
327
  display: flex;
262
328
  align-items: flex-start;
@@ -291,7 +357,7 @@
291
357
  .field-group-label {
292
358
  font-size: var(--field-group-label-font-size, 10px);
293
359
  font-weight: var(--field-group-label-font-weight, 500);
294
- color: var(--field-group-label-color, #9ca3af);
360
+ color: var(--field-group-label-color, #4b5563);
295
361
  text-transform: uppercase;
296
362
  letter-spacing: 0.05em;
297
363
  }
@@ -12,6 +12,29 @@ export type OptionalSplitInputProperties = {
12
12
  separator?: string;
13
13
  testId?: string;
14
14
  classes?: string;
15
+ /**
16
+ * Names the whole group ("One-time code"). The boxes are individually
17
+ * meaningless, so without this a screen-reader user reaches four unlabelled
18
+ * fields; naming the group is what makes them one control.
19
+ */
20
+ ariaLabel?: string;
21
+ /**
22
+ * Text shown, and announced, when the assembled value is in error ("That code
23
+ * is not right"). Referenced by `aria-describedby` on the group rather than on
24
+ * one box, since the message is about the whole code, and sets `aria-invalid`
25
+ * while present.
26
+ */
27
+ errorMessage?: string | null;
28
+ /**
29
+ * Persistent helper text describing the whole group. Referenced the same way,
30
+ * so it is read before the user trips an error rather than only after.
31
+ */
32
+ infoMessage?: string | null;
33
+ /**
34
+ * Marks the group invalid without supplying a message, for a consumer driving
35
+ * validity from a server or its own rules.
36
+ */
37
+ invalid?: boolean;
15
38
  };
16
39
  export type SplitInputEventProperties = {
17
40
  onchange?: (values: string[]) => void;
@@ -444,7 +444,7 @@
444
444
  }
445
445
 
446
446
  .statcard-value-warning {
447
- color: var(--statcard-value-warning-color, #f59e0b);
447
+ color: var(--statcard-value-warning-color, #b45309);
448
448
  }
449
449
 
450
450
  /* Per-row value typography override — falls back to the card-level value
@@ -465,7 +465,7 @@
465
465
  }
466
466
 
467
467
  .statcard-delta-positive {
468
- color: var(--statcard-delta-positive-color, #16a34a);
468
+ color: var(--statcard-delta-positive-color, #166534);
469
469
  }
470
470
 
471
471
  .statcard-delta-negative {
@@ -490,7 +490,7 @@
490
490
  order: var(--statcard-subtitle-order, 0);
491
491
  font-size: var(--statcard-subtitle-font-size, 12px);
492
492
  font-weight: var(--statcard-subtitle-font-weight, 400);
493
- color: var(--statcard-subtitle-color, #9ca3af);
493
+ color: var(--statcard-subtitle-color, #4b5563);
494
494
  line-height: var(--statcard-subtitle-line-height, 1.4);
495
495
  }
496
496
 
@@ -504,7 +504,7 @@
504
504
  --statcard-row-subtitle-font-weight,
505
505
  var(--statcard-subtitle-font-weight, 400)
506
506
  );
507
- color: var(--statcard-row-subtitle-color, var(--statcard-subtitle-color, #9ca3af));
507
+ color: var(--statcard-row-subtitle-color, var(--statcard-subtitle-color, #4b5563));
508
508
  line-height: var(
509
509
  --statcard-row-subtitle-line-height,
510
510
  var(--statcard-subtitle-line-height, 1.4)
@@ -582,7 +582,7 @@
582
582
  does not fit — matches short unit suffixes like "%" or "ms". */
583
583
  font-size: var(--statcard-row-additional-font-size, 12px);
584
584
  font-weight: var(--statcard-row-additional-font-weight, 400);
585
- color: var(--statcard-row-additional-color, #9ca3af);
585
+ color: var(--statcard-row-additional-color, #4b5563);
586
586
  line-height: 1.4;
587
587
  }
588
588
 
@@ -631,7 +631,7 @@
631
631
  .statcard-breakdown-label {
632
632
  font-size: var(--statcard-breakdown-label-font-size, 11px);
633
633
  font-weight: var(--statcard-breakdown-label-font-weight, 400);
634
- color: var(--statcard-breakdown-label-color, #9ca3af);
634
+ color: var(--statcard-breakdown-label-color, #4b5563);
635
635
  line-height: 1.4;
636
636
  }
637
637
 
@@ -92,12 +92,12 @@
92
92
  typography applies, so those two declarations must not sit on the element and win. */
93
93
  .status-text-default {
94
94
  font-weight: var(--status-font-weight, 600);
95
- color: var(--status-description-font-color, #2f3841);
95
+ color: var(--status-description-font-color, light-dark(#3d5a76, #9ab4cc));
96
96
  }
97
97
 
98
98
  .status-description {
99
99
  font-weight: var(--status-font-weight, 400);
100
- color: var(--status-description-font-color, #436484cc);
100
+ color: var(--status-description-font-color, light-dark(#3d5a76, #9ab4cc));
101
101
  padding: 0px 42px;
102
102
  margin-bottom: 25px;
103
103
  }
@@ -138,10 +138,10 @@
138
138
  tokens Step actually renders are set here; the separator is Stepper's to draw. Each
139
139
  chain terminates in the same literal Stepper uses, so the two agree by construction. */
140
140
  .status-completed {
141
- --step-text-color: var(--step-text-completed-color, #24aa5a);
141
+ --step-text-color: var(--step-text-completed-color, #1a7a44);
142
142
  --step-index-container-background-color: var(
143
143
  --step-index-container-completed-background-color,
144
- #24aa5a
144
+ #1a7a44
145
145
  );
146
146
  --step-index-container-border: var(--step-index-container-completed-border, none);
147
147
  }
@@ -198,7 +198,7 @@
198
198
  height: var(--step-index-container-height, 30px);
199
199
  width: var(--step-index-container-width, 30px);
200
200
  border-radius: var(--step-index-container-radius, 50%);
201
- background-color: var(--step-index-container-background-color, #798fa5cc);
201
+ background-color: var(--step-index-container-background-color, #55627a);
202
202
  /* No border today, so the fallback is `none` — an existing consumer that sets
203
203
  nothing renders byte-identically. Shared with .step-icon-container below so
204
204
  a status override (see Stepper.svelte) themes the circle whichever of the
@@ -215,7 +215,7 @@
215
215
  height: var(--step-index-container-height, 30px);
216
216
  width: var(--step-index-container-width, 30px);
217
217
  border-radius: var(--step-index-container-radius, 50%);
218
- background-color: var(--step-index-container-background-color, #798fa5cc);
218
+ background-color: var(--step-index-container-background-color, #55627a);
219
219
  border: var(--step-index-container-border, none);
220
220
  flex-shrink: 0;
221
221
  overflow: hidden;
@@ -230,7 +230,8 @@
230
230
  .step-spinner {
231
231
  width: var(--step-spinner-size, 18px);
232
232
  height: var(--step-spinner-size, 18px);
233
- animation: stepper-spin 0.8s linear infinite;
233
+ animation: stepper-spin var(--step-spinner-animation-duration, var(--motion-duration, 0.8s))
234
+ var(--step-spinner-animation-easing, var(--motion-easing, linear)) infinite;
234
235
  }
235
236
 
236
237
  @keyframes stepper-spin {
@@ -299,7 +300,7 @@
299
300
  `normal` — that is the literal fallback, keeping an existing consumer
300
301
  byte-identical. */
301
302
  font-weight: var(--step-text-font-weight, normal);
302
- color: var(--step-text-color, #798fa5cc);
303
+ color: var(--step-text-color, #55627a);
303
304
  }
304
305
 
305
306
  .step-vertical .step-text {
@@ -318,4 +319,16 @@
318
319
  .step-vertical .step-badge {
319
320
  margin: var(--step-badge-vertical-margin, 4px 0 0 0);
320
321
  }
322
+ /* An indefinite animation is the case this preference exists for: it never
323
+ ends, so a user who asked the OS to minimise motion gets a permanent loop.
324
+ The animation stops, but the element must still read as "busy" -- a guard
325
+ that leaves nothing on screen, or leaves a frame that means something else,
326
+ is worse than the motion it removed. This block lives in the component's own
327
+ <style> because that is the only stylesheet that reaches inside the shadow
328
+ root a custom-element consumer gets. */
329
+ @media (prefers-reduced-motion: reduce) {
330
+ .step-spinner {
331
+ animation: none;
332
+ }
333
+ }
321
334
  </style>