@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
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { AspectRatioProperties } from './properties';
3
+
4
+ let { ratio = 1, children, classes, testId }: AspectRatioProperties = $props();
5
+
6
+ // `ratio` feeds directly into the CSS `aspect-ratio` declaration below, which is
7
+ // only meaningful for a finite, positive number: zero/negative have no geometric
8
+ // meaning, and NaN/Infinity (e.g. an image's naturalWidth / naturalHeight read
9
+ // before it has decoded) would serialize to an invalid token and drop the whole
10
+ // declaration. Checked independently of the `= 1` default parameter above -- that
11
+ // default only fires when the prop is omitted entirely, so an explicitly passed
12
+ // 0/-2/NaN/Infinity still reaches this component and needs the same fallback.
13
+ let resolvedRatio = $derived(Number.isFinite(ratio) && ratio > 0 ? ratio : 1);
14
+ </script>
15
+
16
+ <div
17
+ class="container {classes ?? ''}"
18
+ style:aspect-ratio="var(--aspect-ratio-container-aspect-ratio, {resolvedRatio})"
19
+ data-pw={typeof testId === 'string' ? testId : null}
20
+ testID={typeof testId === 'string' ? testId : null}
21
+ >
22
+ {@render children?.()}
23
+ </div>
24
+
25
+ <style>
26
+ /*
27
+ * The CSS `aspect-ratio` property is used unconditionally, with no
28
+ * `padding-bottom` percentage-hack fallback. It has shipped in every
29
+ * evergreen browser since 2021 (Chrome 88, Firefox 89, Safari 15) -- the
30
+ * same generation of CSS this library already relies on elsewhere with no
31
+ * fallback path (Accordion's expand/collapse animates
32
+ * `grid-template-rows` between `0fr` and `1fr`, which needs the same-era
33
+ * interpolation support). A `padding-bottom` fallback would also
34
+ * reintroduce the wrapper-plus-absolutely-positioned-child structure
35
+ * `aspect-ratio` exists to remove, doubling the markup this component
36
+ * would carry for browsers this library does not otherwise support.
37
+ *
38
+ * The ratio itself is set inline (see the `style:aspect-ratio` binding in
39
+ * the markup) rather than here, because its fallback is the per-instance
40
+ * `ratio` prop value, not a fixed default -- but the inline value always
41
+ * routes through the `--aspect-ratio-container-aspect-ratio` custom
42
+ * property first, so a consumer can still override the ratio purely
43
+ * through CSS (e.g. set it on an ancestor, or on this element via
44
+ * `classes`) without touching the prop at all.
45
+ */
46
+ .container {
47
+ display: block;
48
+ width: var(--aspect-ratio-container-width, 100%);
49
+ overflow: var(--aspect-ratio-container-overflow, hidden);
50
+ }
51
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const AspectRatio: import("svelte").Component<import("./properties").OptionalAspectRatioProperties, {}, "">;
2
+ type AspectRatio = ReturnType<typeof AspectRatio>;
3
+ export default AspectRatio;
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type AspectRatioProperties = OptionalAspectRatioProperties;
3
+ export type MandatoryAspectRatioProperties = Record<string, never>;
4
+ export type OptionalAspectRatioProperties = {
5
+ /**
6
+ * Width-to-height ratio the content is constrained to, expressed the same
7
+ * way the CSS `aspect-ratio` property reads it -- e.g. `16 / 9` for a
8
+ * widescreen frame, `4 / 3` for a photo. Must be a finite, positive
9
+ * number: zero and negative ratios have no geometric meaning, and a
10
+ * non-finite value (`NaN`, `Infinity` -- e.g. from an image's
11
+ * `naturalWidth / naturalHeight` read before it has decoded) would either
12
+ * produce an invalid CSS declaration or a degenerate one-dimensional box.
13
+ * An invalid `ratio` falls back to the default of `1` (a square) rather
14
+ * than emitting broken CSS.
15
+ */
16
+ ratio?: number;
17
+ /**
18
+ * Content rendered inside the ratio-constrained container. Optional --
19
+ * an empty `AspectRatio` still reserves the correctly-shaped layout space,
20
+ * which is useful as a placeholder while the real content (e.g. an image)
21
+ * is still loading.
22
+ */
23
+ children?: Snippet;
24
+ classes?: string;
25
+ /** Renders as `data-pw` (and `testID`) on the root element for Playwright test selection. */
26
+ testId?: string;
27
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,9 @@
1
1
  <script lang="ts">
2
+ // A Svelte transition directive takes a JS number, so no `@media
3
+ // (prefers-reduced-motion: reduce)` block can reach it -- the guard has to be
4
+ // in script. Duration 0 lands on exactly the frame the slide was travelling
5
+ // to: the banner, present. Nothing is stranded.
6
+ import { prefersReducedMotion } from '../utils';
2
7
  import type { BannerProperties } from './properties';
3
8
  import { slide } from 'svelte/transition';
4
9
  import Button from '../Button/Button.svelte';
@@ -17,7 +22,8 @@
17
22
  ondismiss,
18
23
  classes,
19
24
  title,
20
- role = null
25
+ role = null,
26
+ transitionDuration
21
27
  }: BannerProperties = $props();
22
28
 
23
29
  let interactive = $derived(typeof onclick === 'function');
@@ -54,7 +60,7 @@
54
60
  tabindex={interactive ? 0 : null}
55
61
  data-pw={typeof testId === 'string' ? testId : null}
56
62
  testID={typeof testId === 'string' ? testId : null}
57
- transition:slide={{ duration: 300 }}
63
+ transition:slide={{ duration: prefersReducedMotion() ? 0 : (transitionDuration ?? 300) }}
58
64
  >
59
65
  {#if typeof icon === 'function'}
60
66
  <div class="banner-icon">
@@ -109,7 +115,7 @@
109
115
  justify-content: var(--banner-justify-content, center);
110
116
  background-color: var(--banner-background, #f0f4f8);
111
117
  color: var(--banner-color, #4d6174);
112
- font-family: var(--banner-font-family);
118
+ font-family: var(--banner-font-family, inherit);
113
119
  font-size: var(--banner-font-size, 14px);
114
120
  font-weight: var(--banner-font-weight, 500);
115
121
  line-height: var(--banner-line-height, 1.3);
@@ -156,7 +162,7 @@
156
162
  }
157
163
 
158
164
  .banner-link-text {
159
- color: var(--banner-link-color, #0099ff);
165
+ color: var(--banner-link-color, #1d4ed8);
160
166
  margin-left: var(--banner-link-gap, 4px);
161
167
  }
162
168
 
@@ -14,6 +14,14 @@ export type OptionalBannerProperties = {
14
14
  dismissIcon?: Snippet;
15
15
  classes?: string;
16
16
  role?: string | null;
17
+ /**
18
+ * Duration (ms) of the show/hide `slide` transition. A CSS custom property
19
+ * cannot reach a Svelte transition directive's parameters, so this prop is
20
+ * the token-contract equivalent for Banner's motion — same shape as
21
+ * Toast's `inAnimationDuration`/`outAnimationDuration`. Defaults to the
22
+ * library's existing 300ms so omitting it renders identically to before.
23
+ */
24
+ transitionDuration?: number | null;
17
25
  };
18
26
  export type BannerEventProperties = {
19
27
  onclick?: (event: MouseEvent) => void;
@@ -104,10 +104,17 @@
104
104
  let plotEl: HTMLDivElement | null = $state(null);
105
105
  let chartWidth = $state(0);
106
106
  let chartHeight = $state(0);
107
+ // `hovered` (pointer) and `focused` (keyboard) are kept as separate state
108
+ // rather than one field shared by both input sources: tabbing to a bar
109
+ // fires a native scroll-into-view, which triggers a late `pointerleave` on
110
+ // whatever the mouse used to sit over -- with one shared field that
111
+ // pointerleave unconditionally clobbers the just-set focus highlight. See
112
+ // `activeBar` below for the precedence this restores (focus wins over
113
+ // pointer hover).
107
114
  let hovered = $state<{ si: number; pi: number } | null>(null);
115
+ let focused = $state<{ si: number; pi: number } | null>(null);
108
116
  let mouseX = $state(0);
109
117
  let mouseY = $state(0);
110
- let anchor = $state<TooltipAnchor | null>(null);
111
118
  let valueFontSize = $state(14);
112
119
  /** Internal tracking for the imperative highlight API (onchartready). */
113
120
  let apiHighlightedIndex = $state<number | null>(null);
@@ -247,6 +254,17 @@
247
254
 
248
255
  let isStackedMode = $derived(isMulti && groupMode === 'stacked');
249
256
 
257
+ /**
258
+ * A stacked bar's contribution to its category total and running baseline.
259
+ * Mirrors computeStackedValues' precedent (geometry.ts): in a value-accumulating
260
+ * stack, a negative or non-finite value has no well-defined height, so it
261
+ * contributes 0 rather than poisoning stackBase for every series stacked
262
+ * above it in the same category (Math.max(0, NaN) is NaN, not 0).
263
+ */
264
+ function stackContribution(value: number): number {
265
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
266
+ }
267
+
250
268
  function getDisplayValue(bar: BarRect): string {
251
269
  if (typeof bar.dataPoint.valueLabel === 'string' && bar.dataPoint.valueLabel.length > 0) {
252
270
  return bar.dataPoint.valueLabel;
@@ -271,7 +289,10 @@
271
289
  return [0, 100];
272
290
  }
273
291
  const totalsPerLabel = labels.map((_, labelIndex) =>
274
- visibleEntries.reduce((sum, { s }) => sum + Math.max(0, s.data[labelIndex]?.value ?? 0), 0)
292
+ visibleEntries.reduce(
293
+ (sum, { s }) => sum + stackContribution(s.data[labelIndex]?.value ?? 0),
294
+ 0
295
+ )
275
296
  );
276
297
  return niceLinearDomain(0, Math.max(0, ...totalsPerLabel));
277
298
  }
@@ -286,10 +307,15 @@
286
307
  }
287
308
  }
288
309
  }
289
- if (all.length === 0) {
310
+ // Non-finite values (NaN/Infinity) mark gap points; exclude them here so
311
+ // one bad point cannot poison the whole extent (Math.min/max propagate
312
+ // NaN, and Infinity passes right through a max comparison). Matches the
313
+ // LineChart/AreaChart call-site filter.
314
+ const finite = all.filter((v) => Number.isFinite(v));
315
+ if (finite.length === 0) {
290
316
  return [0, 1];
291
317
  }
292
- return niceLinearDomain(Math.min(0, ...all), Math.max(0, ...all));
318
+ return niceLinearDomain(Math.min(0, ...finite), Math.max(0, ...finite));
293
319
  });
294
320
 
295
321
  let valTickCount = $derived(
@@ -414,7 +440,10 @@
414
440
  }
415
441
  } else if (isMulti && groupMode === 'stacked') {
416
442
  const categoryTotals = labels.map((_, labelIndex) =>
417
- visibleEntries.reduce((sum, { s }) => sum + Math.max(0, s.data[labelIndex]?.value ?? 0), 0)
443
+ visibleEntries.reduce(
444
+ (sum, { s }) => sum + stackContribution(s.data[labelIndex]?.value ?? 0),
445
+ 0
446
+ )
418
447
  );
419
448
  const stackBase = new Array(labels.length).fill(0);
420
449
  for (let vi = 0; vi < visibleEntries.length; vi++) {
@@ -422,7 +451,7 @@
422
451
  const seriesFill: BarFill = s.color ?? getColor(si);
423
452
  for (let pi = 0; pi < s.data.length; pi++) {
424
453
  const d = s.data[pi];
425
- const rawVal = Math.max(0, d.value);
454
+ const rawVal = stackContribution(d.value);
426
455
  const normalizedValue = isNormalized
427
456
  ? categoryTotals[pi] > 0
428
457
  ? (rawVal / categoryTotals[pi]) * 100
@@ -676,11 +705,31 @@
676
705
 
677
706
  // ── Tooltip ────────────────────────────────────────────────────
678
707
 
708
+ /**
709
+ * The bar's accessible name, mirroring `tooltipData`'s own title rule below.
710
+ *
711
+ * In a multi-series chart the tooltip disambiguates two bars in the same
712
+ * category as `{label} — {seriesName}`; the aria-label did not, so every bar
713
+ * in a category announced identically. A screen-reader user tabbing a grouped
714
+ * chart heard "Jan: 42" then "Jan: 38" with nothing saying which series either
715
+ * belonged to -- a distinction sighted users have had since the tooltip
716
+ * shipped. The two spellings live next to each other so they cannot drift
717
+ * apart again.
718
+ */
719
+ function barAccessibleName(bar: BarRect): string {
720
+ const label = isMulti ? `${bar.dataPoint.label} — ${bar.seriesName}` : bar.dataPoint.label;
721
+ return `${label}: ${getDisplayValue(bar)}`;
722
+ }
723
+
724
+ // Precedence: keyboard focus wins over pointer hover (see the state
725
+ // declarations above for why they're tracked separately).
726
+ let activeBar = $derived(focused ?? hovered);
727
+
679
728
  let tooltipData = $derived.by(() => {
680
- if (hovered === null) {
729
+ if (activeBar === null) {
681
730
  return null;
682
731
  }
683
- const bar = bars.find((b) => b.si === hovered!.si && b.pi === hovered!.pi);
732
+ const bar = bars.find((b) => b.si === activeBar!.si && b.pi === activeBar!.pi);
684
733
  if (!bar) {
685
734
  return null;
686
735
  }
@@ -725,11 +774,6 @@
725
774
 
726
775
  // ── Interactions ───────────────────────────────────────────────
727
776
 
728
- // Narrows an event's currentTarget to Element without an `as` cast (repo
729
- // lint bans type assertions outside test files).
730
- const targetElement = (e: Event): Element | null =>
731
- e.currentTarget instanceof Element ? e.currentTarget : null;
732
-
733
777
  function trackMouse(e: PointerEvent): void {
734
778
  const position = pointerPositionIn(plotEl, e);
735
779
  if (position !== null) {
@@ -751,31 +795,86 @@
751
795
  : { x: r.right - c.left, y: r.top + r.height / 2 - c.top, side: 'right' };
752
796
  }
753
797
 
754
- function activateBar(target: Element, bar: BarRect): void {
798
+ // Looks up the live DOM element for the active bar via the `data-pw`
799
+ // index attribute every bar path already renders (bar-${i}), rather than
800
+ // caching an element reference from whichever handler last fired. Needed
801
+ // because `anchor` below must fall back correctly even when focus blurs
802
+ // off a bar while the pointer independently, silently, still rests over a
803
+ // different (already-hovered but never re-entered) bar -- a blur event
804
+ // carries no reference to that other bar's element.
805
+ function findBarElement(target: { si: number; pi: number }): Element | null {
806
+ if (plotEl === null) {
807
+ return null;
808
+ }
809
+ const idx = bars.findIndex((b) => b.si === target.si && b.pi === target.pi);
810
+ if (idx === -1) {
811
+ return null;
812
+ }
813
+ return plotEl.querySelector(`[data-pw="bar-${idx}"]`);
814
+ }
815
+
816
+ let anchor = $derived.by<TooltipAnchor | null>(() => {
817
+ if (activeBar === null) {
818
+ return null;
819
+ }
820
+ const el = findBarElement(activeBar);
821
+ return el ? anchorFromElement(el) : null;
822
+ });
823
+
824
+ function sameBar(
825
+ a: { si: number; pi: number } | null,
826
+ b: { si: number; pi: number } | null
827
+ ): boolean {
828
+ return a === b || (a !== null && b !== null && a.si === b.si && a.pi === b.pi);
829
+ }
830
+
831
+ // Mirrors the combined activeBar (focus-or-hover) to onbarhover, deduped
832
+ // so that e.g. focus landing on the bar the pointer already hovers doesn't
833
+ // re-fire the callback with a value that never went away.
834
+ let lastNotifiedBar: { si: number; pi: number } | null = null;
835
+ function notifyBarHover(): void {
836
+ const next = activeBar;
837
+ if (sameBar(next, lastNotifiedBar)) {
838
+ return;
839
+ }
840
+ lastNotifiedBar = next;
841
+ if (next === null) {
842
+ onbarhover?.(null);
843
+ return;
844
+ }
845
+ const bar = bars.find((b) => b.si === next.si && b.pi === next.pi);
846
+ if (bar) {
847
+ onbarhover?.({ index: bar.pi, dataPoint: bar.dataPoint });
848
+ }
849
+ }
850
+
851
+ function activatePointer(bar: BarRect): void {
755
852
  hovered = { si: bar.si, pi: bar.pi };
756
- anchor = anchorFromElement(target);
757
- onbarhover?.({ index: bar.pi, dataPoint: bar.dataPoint });
853
+ notifyBarHover();
854
+ }
855
+
856
+ function activateFocus(bar: BarRect): void {
857
+ focused = { si: bar.si, pi: bar.pi };
858
+ notifyBarHover();
758
859
  }
759
860
 
760
861
  function handleEnter(e: PointerEvent, bar: BarRect): void {
761
- const el = targetElement(e);
762
- if (el !== null) {
763
- activateBar(el, bar);
764
- }
862
+ activatePointer(bar);
765
863
  trackMouse(e);
766
864
  }
767
865
 
768
- function handleFocus(e: FocusEvent, bar: BarRect): void {
769
- const el = targetElement(e);
770
- if (el !== null) {
771
- activateBar(el, bar);
772
- }
866
+ function handleFocus(bar: BarRect): void {
867
+ activateFocus(bar);
773
868
  }
774
869
 
775
- function handleLeave(): void {
870
+ function handlePointerLeave(): void {
776
871
  hovered = null;
777
- anchor = null;
778
- onbarhover?.(null);
872
+ notifyBarHover();
873
+ }
874
+
875
+ function handleBlur(): void {
876
+ focused = null;
877
+ notifyBarHover();
779
878
  }
780
879
 
781
880
  function handleKeydown(e: KeyboardEvent, bar: BarRect): void {
@@ -785,23 +884,30 @@
785
884
  }
786
885
  }
787
886
 
788
- // Touch taps have no pointerleave: dismiss when a pointerdown lands outside.
887
+ // Touch taps have no pointerleave: dismiss when a pointerdown lands
888
+ // outside. Clears only the hover half -- a real pointerdown outside a
889
+ // focused bar blurs it natively (see handleBlur), matching
890
+ // PieChart/AreaChart/LineChart's identical hover-only scope for this
891
+ // effect.
789
892
  // eslint-disable-next-line no-restricted-syntax
790
893
  $effect(() => {
791
894
  if (hovered === null) {
792
895
  return;
793
896
  }
794
- return dismissOnOutsidePointerDown(containerEl, handleLeave);
897
+ return dismissOnOutsidePointerDown(containerEl, () => {
898
+ hovered = null;
899
+ notifyBarHover();
900
+ });
795
901
  });
796
902
 
797
903
  function handleClick(bar: BarRect) {
798
904
  onbarclick?.({ index: bar.pi, dataPoint: bar.dataPoint });
799
905
  }
800
906
 
801
- function hoveredBar() {
802
- return hovered === null
907
+ function activeBarData() {
908
+ return activeBar === null
803
909
  ? null
804
- : (bars.find((b) => b.si === hovered!.si && b.pi === hovered!.pi) ?? null);
910
+ : (bars.find((b) => b.si === activeBar!.si && b.pi === activeBar!.pi) ?? null);
805
911
  }
806
912
  </script>
807
913
 
@@ -955,11 +1061,11 @@
955
1061
  {#if isStackedMode && barRadius > 0}
956
1062
  <path
957
1063
  class="bar"
958
- class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
1064
+ class:hovered={activeBar?.si === bar.si && activeBar?.pi === bar.pi}
959
1065
  class:highlighted={effectiveHighlightedIndex !== null &&
960
1066
  effectiveHighlightedIndex === bar.pi}
961
- class:dimmed={(hovered !== null &&
962
- (hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
1067
+ class:dimmed={(activeBar !== null &&
1068
+ (activeBar.si !== bar.si || activeBar.pi !== bar.pi)) ||
963
1069
  (effectiveHighlightedIndex !== null &&
964
1070
  effectiveHighlightedIndex !== bar.pi)}
965
1071
  d={stackedBarPath(bar)}
@@ -968,23 +1074,23 @@
968
1074
  testID={`bar-${i}`}
969
1075
  tabindex="0"
970
1076
  role="button"
971
- aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
1077
+ aria-label={barAccessibleName(bar)}
972
1078
  onpointerenter={(e) => handleEnter(e, bar)}
973
1079
  onpointermove={trackMouse}
974
- onpointerleave={handleLeave}
975
- onfocus={(e) => handleFocus(e, bar)}
976
- onblur={handleLeave}
1080
+ onpointerleave={handlePointerLeave}
1081
+ onfocus={() => handleFocus(bar)}
1082
+ onblur={handleBlur}
977
1083
  onkeydown={(e) => handleKeydown(e, bar)}
978
1084
  onclick={() => handleClick(bar)}
979
1085
  />
980
1086
  {:else}
981
1087
  <path
982
1088
  class="bar"
983
- class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
1089
+ class:hovered={activeBar?.si === bar.si && activeBar?.pi === bar.pi}
984
1090
  class:highlighted={effectiveHighlightedIndex !== null &&
985
1091
  effectiveHighlightedIndex === bar.pi}
986
- class:dimmed={(hovered !== null &&
987
- (hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
1092
+ class:dimmed={(activeBar !== null &&
1093
+ (activeBar.si !== bar.si || activeBar.pi !== bar.pi)) ||
988
1094
  (effectiveHighlightedIndex !== null &&
989
1095
  effectiveHighlightedIndex !== bar.pi)}
990
1096
  d={valueEndBarPath(bar)}
@@ -993,12 +1099,12 @@
993
1099
  testID={`bar-${i}`}
994
1100
  tabindex="0"
995
1101
  role="button"
996
- aria-label="{bar.dataPoint.label}: {getDisplayValue(bar)}"
1102
+ aria-label={barAccessibleName(bar)}
997
1103
  onpointerenter={(e) => handleEnter(e, bar)}
998
1104
  onpointermove={trackMouse}
999
- onpointerleave={handleLeave}
1000
- onfocus={(e) => handleFocus(e, bar)}
1001
- onblur={handleLeave}
1105
+ onpointerleave={handlePointerLeave}
1106
+ onfocus={() => handleFocus(bar)}
1107
+ onblur={handleBlur}
1002
1108
  onkeydown={(e) => handleKeydown(e, bar)}
1003
1109
  onclick={() => handleClick(bar)}
1004
1110
  />
@@ -1044,7 +1150,7 @@
1044
1150
  unstyled
1045
1151
  >
1046
1152
  {#snippet content()}
1047
- {@const hb = hoveredBar()}
1153
+ {@const hb = activeBarData()}
1048
1154
  {#if hb}
1049
1155
  {@render tooltipSnippet(hb.dataPoint, hb.pi)}
1050
1156
  {/if}
@@ -1076,7 +1182,8 @@
1076
1182
  position: relative;
1077
1183
  }
1078
1184
  .bar {
1079
- transition: opacity var(--chart-transition-duration, 0.2s) ease;
1185
+ transition: opacity var(--chart-transition-duration, var(--motion-duration, 0.2s))
1186
+ var(--chart-transition-easing, var(--motion-easing, ease));
1080
1187
  cursor: pointer;
1081
1188
  }
1082
1189
  .bar.hovered {
@@ -273,10 +273,23 @@
273
273
  border: none;
274
274
  padding: 0;
275
275
  cursor: pointer;
276
- transition: background-color 0.2s ease;
276
+ transition: background-color var(--book-dot-transition-duration, var(--motion-duration, 0.2s))
277
+ var(--book-dot-transition-easing, var(--motion-easing, ease));
277
278
  }
278
279
 
279
280
  .dot-active {
280
281
  background-color: var(--book-dot-active-color, #333333);
281
282
  }
283
+
284
+ /* The page turn is a transform, which is the movement prefers-reduced-motion
285
+ exists for. Both ends of the transition are meaningful on their own -- a page
286
+ is either turned or not -- so removing it leaves no misleading frame, unlike
287
+ the timed loaders in Button and ListItem. This block lives in the component's
288
+ own <style> because that is the only stylesheet reaching inside the shadow
289
+ root a custom-element consumer gets. */
290
+ @media (prefers-reduced-motion: reduce) {
291
+ .page {
292
+ transition: none;
293
+ }
294
+ }
282
295
  </style>
@@ -26,8 +26,14 @@
26
26
 
27
27
  <style>
28
28
  .background {
29
- height: var(--loader-background-height);
30
- width: var(--loader-background-width);
29
+ height: var(--loader-background-height, auto);
30
+ width: var(--loader-background-width, auto);
31
+ /* Caps the splash against whatever box it was placed in. min-width:0 is
32
+ required as well: as a flex item this would otherwise refuse to shrink
33
+ below the 100vw min-content of .loader inside it, and the cap would
34
+ resolve against a width .loader had itself dictated. */
35
+ max-width: 100%;
36
+ min-width: 0;
31
37
  border-radius: var(--loader-background-border-radius, 0px);
32
38
  }
33
39
 
@@ -42,6 +48,12 @@
42
48
  height: var(--brand-loader-height, var(--loader-height, 100vh));
43
49
  width: var(--brand-loader-width, var(--loader-width, 100vw));
44
50
  border-radius: var(--loader-background-border-radius, 0px);
51
+ /* The 100vw/100vh default above is the full-screen splash case. An embedded
52
+ instance must never exceed the box it was placed in: this is a centring
53
+ flex container, so overflowing it centres the logo on the viewport rather
54
+ than on the container and the content visibly drifts to one side. */
55
+ max-width: 100%;
56
+ max-height: 100%;
45
57
  display: flex;
46
58
  justify-content: var(--loader-justify-content, center);
47
59
  align-items: var(--loader-align-items, center);
@@ -56,21 +68,35 @@
56
68
  .text {
57
69
  font-size: var(--loader-text-font-size, 22px);
58
70
  padding: var(--loader-text-padding, 16px 0px);
59
- color: var(--loader-text-color, white);
71
+ /* The frosted overlay below is only ~20% opaque, so its painted colour is
72
+ mostly whatever page sits behind it. A splash screen is far more often
73
+ hosted on a light page than over dark imagery, so the readable-by-default
74
+ choice is a dark tone here; --loader-text-color stays the override point
75
+ for the over-dark-imagery look (e.g. --loader-text-color: white). */
76
+ color: var(--loader-text-color, #333333);
60
77
  font-family: var(--loader-text-font, inherit);
61
78
  }
62
79
 
63
80
  .sub-text {
64
81
  font-size: var(--loader-sub-text-font-size, 12px);
65
82
  margin: var(--loader-sub-text-margin, 16px);
83
+ /* Previously unset, so it silently inherited whatever colour the host page
84
+ happened to have -- legible by accident on this doc site, invisible or
85
+ wrong wherever that assumption didn't hold. Same reasoning as .text: a
86
+ muted dark tone reads on the default light backdrop. */
87
+ color: var(--loader-sub-text-color, #52525b);
66
88
  }
67
89
 
90
+ /* `.background` used to declare `animation: animateBackground ...` here, but
91
+ `@keyframes animateBackground` has never existed anywhere in this repo --
92
+ confirmed back to 8801087c, the commit that introduced BrandLoader (Nov
93
+ 2023). It referenced nothing and so never animated anything; this was
94
+ dead from day one, not a regression, and no prior design for what it
95
+ should look like ever shipped, so it is removed rather than reconstructed.
96
+ The `prefers-reduced-motion` override below is left in place as a guard:
97
+ if a real indefinite animation is ever added to `.background`, that guard
98
+ already stops it. */
68
99
  @supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) {
69
- .background {
70
- animation: animateBackground 5s ease-in-out infinite normal;
71
- -webkit-animation: animateBackground 5s ease-in-out infinite normal;
72
- }
73
-
74
100
  .loader {
75
101
  background-color: var(--loader-background-color, #ffffff33);
76
102
  -webkit-backdrop-filter: blur(var(--loader-webkit-backdrop-filter, 50px));
@@ -96,19 +122,23 @@
96
122
  }
97
123
  .lds-ellipsis div:nth-child(1) {
98
124
  left: 8px;
99
- animation: lds-ellipsis1 0.6s infinite;
125
+ animation: lds-ellipsis1
126
+ var(--brand-loader-ellipsis-animation-duration, var(--motion-duration, 0.6s)) infinite;
100
127
  }
101
128
  .lds-ellipsis div:nth-child(2) {
102
129
  left: 8px;
103
- animation: lds-ellipsis2 0.6s infinite;
130
+ animation: lds-ellipsis2
131
+ var(--brand-loader-ellipsis-animation-duration, var(--motion-duration, 0.6s)) infinite;
104
132
  }
105
133
  .lds-ellipsis div:nth-child(3) {
106
134
  left: 32px;
107
- animation: lds-ellipsis2 0.6s infinite;
135
+ animation: lds-ellipsis2
136
+ var(--brand-loader-ellipsis-animation-duration, var(--motion-duration, 0.6s)) infinite;
108
137
  }
109
138
  .lds-ellipsis div:nth-child(4) {
110
139
  left: 56px;
111
- animation: lds-ellipsis3 0.6s infinite;
140
+ animation: lds-ellipsis3
141
+ var(--brand-loader-ellipsis-animation-duration, var(--motion-duration, 0.6s)) infinite;
112
142
  }
113
143
  @keyframes lds-ellipsis1 {
114
144
  0% {
@@ -134,4 +164,38 @@
134
164
  transform: translate(24px, 0);
135
165
  }
136
166
  }
167
+ /* An indefinite animation is the case this preference exists for: it never
168
+ ends, so a user who asked the OS to minimise motion gets a permanent loop.
169
+ The animation stops, but the element must still read as "busy" -- a guard
170
+ that leaves nothing on screen, or leaves a frame that means something else,
171
+ is worse than the motion it removed. This block lives in the component's own
172
+ <style> because that is the only stylesheet that reaches inside the shadow
173
+ root a custom-element consumer gets. */
174
+ @media (prefers-reduced-motion: reduce) {
175
+ .background {
176
+ -webkit-animation: none;
177
+ animation: none;
178
+ }
179
+
180
+ /* Each dot's animation is declared on a `:nth-child()` selector, which
181
+ outranks a bare `.lds-ellipsis div` rule and comes later, so the guard
182
+ has to name them at the same specificity or it is silently overridden. */
183
+ .lds-ellipsis div:nth-child(1),
184
+ .lds-ellipsis div:nth-child(2),
185
+ .lds-ellipsis div:nth-child(3),
186
+ .lds-ellipsis div:nth-child(4) {
187
+ /* Two of the four dots animate scale (one in, one out) and two translate.
188
+ With the animations stopped every transform resolves to its identity,
189
+ so all four sit at their `left` offsets at full size. */
190
+ animation: none;
191
+ transform: none;
192
+ }
193
+
194
+ .lds-ellipsis div:nth-child(1) {
195
+ /* The scale-in dot shares `left: 8px` with the second one, so at rest the
196
+ two occupy the same spot and the row reads as three dots with one drawn
197
+ twice. Dropping it leaves three, evenly spaced at 8/32/56. */
198
+ display: none;
199
+ }
200
+ }
137
201
  </style>