@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
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Attaches hover and focus listeners to the host element without injecting any wrapper
5
5
  * div, which prevents flex-child sizing breakage inside toolbars and icon rows.
6
- * The tooltip bubble is mounted directly on `document.body` with `position:fixed`
7
- * coordinates derived from `getBoundingClientRect`, so it is never clipped by
8
- * `overflow:hidden` ancestors.
6
+ * The tooltip bubble is mounted directly on the host node's own root — `document.body`,
7
+ * or the shadow root when the host is inside one (see `portalTarget`) — with
8
+ * `position:fixed` coordinates derived from `getBoundingClientRect`, so it is never
9
+ * clipped by `overflow:hidden` ancestors.
9
10
  *
10
11
  * Usage:
11
12
  * ```svelte
@@ -17,6 +18,40 @@
17
18
  */
18
19
  /** Monotonically-incrementing counter used to generate unique tooltip bubble IDs. */
19
20
  let tooltipIdCounter = 0;
21
+ /**
22
+ * Where the bubble is allowed to land, asked of the HOST node rather than the
23
+ * bubble: a freshly created, still-detached element is its own root, so it can
24
+ * answer nothing useful about where the component lives.
25
+ *
26
+ * This action has no in-place fallback to fall back to -- not injecting a wrapper
27
+ * div is its entire reason to exist -- so the bubble is portalled either way, and
28
+ * the only question is where. `document.body` is the wrong answer from inside a
29
+ * `<sui-*>` element three times over: the `--tooltip-*` custom properties the
30
+ * inline styles below read inherit from the shadow host, not from `<body>`, so a
31
+ * themed host reverts to the literal fallbacks; `options.classes` names a class
32
+ * whose stylesheet is in the shadow root the bubble just left; and
33
+ * `aria-describedby` is an IDREF, which does not cross a shadow boundary at all,
34
+ * so the association the ARIA tooltip pattern requires simply does not exist.
35
+ * The node's own root keeps all three.
36
+ *
37
+ * Both the `ShadowRoot` constructor and `getRootNode` are checked for rather
38
+ * than assumed, which the five component copies of this helper do not need to
39
+ * do. Unlike them, this module is exercised outside a DOM: tooltip-action.test.ts
40
+ * declares `@vitest-environment node` and drives the action with a hand-rolled
41
+ * element stub carrying only the handful of methods it uses, so `ShadowRoot` is
42
+ * not a global there and the stub has no `getRootNode`. Either way the answer is
43
+ * the same one the pre-existing code gave -- a node that cannot say where it
44
+ * lives is not in a shadow root as far as this can tell. Duck-typing over an
45
+ * `instanceof` check is the idiom `_interaction/focus.ts` already sets for
46
+ * exactly this reason.
47
+ */
48
+ const portalTarget = (node) => {
49
+ if (typeof ShadowRoot === 'undefined' || typeof node.getRootNode !== 'function') {
50
+ return document.body;
51
+ }
52
+ const root = node.getRootNode();
53
+ return root instanceof ShadowRoot ? root : document.body;
54
+ };
20
55
  export const tooltip = (node, options) => {
21
56
  let currentOptions = { ...options };
22
57
  let bubbleEl = null;
@@ -84,7 +119,7 @@ export const tooltip = (node, options) => {
84
119
  textNode.textContent = currentOptions.text;
85
120
  bubbleEl.appendChild(arrowEl);
86
121
  bubbleEl.appendChild(textNode);
87
- document.body.appendChild(bubbleEl);
122
+ portalTarget(node).appendChild(bubbleEl);
88
123
  };
89
124
  /**
90
125
  * Compute and apply `top`/`left` fixed coordinates plus arrow styles based on the
@@ -7,6 +7,8 @@
7
7
  text,
8
8
  speed = 15,
9
9
  isStreaming = false,
10
+ markdown = false,
11
+ markdownOptions,
10
12
  renderText,
11
13
  variableDelay,
12
14
  resolveDelay,
@@ -16,6 +18,23 @@
16
18
  classes
17
19
  }: TypewriterTextProperties = $props();
18
20
 
21
+ // Keep the promise stable across streamed prop updates so the await block does
22
+ // not flash escaped source between already-rendered Markdown frames.
23
+ let rendererPromise: Promise<typeof import('../MarkdownText/markdown')> | null = null;
24
+ function loadMarkdownRenderer(): Promise<typeof import('../MarkdownText/markdown')> {
25
+ rendererPromise ??= import('../MarkdownText/markdown');
26
+ return rendererPromise;
27
+ }
28
+ const markdownRenderer = $derived(markdown === true ? loadMarkdownRenderer() : null);
29
+ // Object-valued custom-element props may receive JSON null or other non-objects.
30
+ const safeMarkdownOptions = $derived(
31
+ typeof markdownOptions === 'object' &&
32
+ markdownOptions !== null &&
33
+ !Array.isArray(markdownOptions)
34
+ ? markdownOptions
35
+ : {}
36
+ );
37
+
19
38
  let displayedText = $state('');
20
39
  let currentIndex = $state(0);
21
40
  let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
@@ -182,11 +201,24 @@
182
201
 
183
202
  <div
184
203
  class="typewriter-text {classes ?? ''}"
185
- class:plain={typeof renderText !== 'function'}
204
+ class:plain={markdown !== true && typeof renderText !== 'function'}
186
205
  data-pw={typeof testId === 'string' ? testId : null}
187
206
  testID={typeof testId === 'string' ? testId : null}
188
207
  >
189
- {#if typeof renderText === 'function'}
208
+ {#if markdown === true}
209
+ {#await markdownRenderer}
210
+ {displayedText}
211
+ {:then renderer}
212
+ {#if renderer !== null}
213
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
214
+ {@html renderer.renderMarkdown(displayedText, safeMarkdownOptions)}
215
+ {:else}
216
+ {displayedText}
217
+ {/if}
218
+ {:catch}
219
+ {displayedText}
220
+ {/await}
221
+ {:else if typeof renderText === 'function'}
190
222
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
191
223
  {@html renderText(displayedText)}
192
224
  {:else if renderCharacter}
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { RenderMarkdownOptions } from '../MarkdownText/properties';
2
3
  export type TypewriterTextProperties = OptionalTypewriterTextProperties & MandatoryTypewriterTextProperties;
3
4
  export type MandatoryTypewriterTextProperties = {
4
5
  text: string;
@@ -74,9 +75,21 @@ export type OptionalTypewriterTextProperties = {
74
75
  */
75
76
  isStreaming?: boolean;
76
77
  /**
77
- * Renders the revealed text as HTML — pass a markdown renderer to type rich text.
78
- * The component trusts the returned string, so sanitise inside the renderer if the
79
- * text can contain untrusted input. When omitted, the text renders as plain text.
78
+ * Render every revealed prefix through the library's safe Markdown pipeline.
79
+ * Raw HTML is escaped and unsafe link/image protocols are stripped. Defaults to
80
+ * false; when true, takes precedence over both renderText and renderCharacter.
81
+ */
82
+ markdown?: boolean;
83
+ /**
84
+ * Options for safe Markdown rendering, including narrower sanitization policies.
85
+ * Only used when markdown is true; never applied to a trusted renderText callback.
86
+ */
87
+ markdownOptions?: RenderMarkdownOptions;
88
+ /**
89
+ * Trusted custom-HTML escape hatch. The caller owns the safety of every returned
90
+ * string, including incomplete streaming prefixes; sanitize untrusted input inside
91
+ * the callback. markdownOptions do not apply here. Ignored when markdown is true;
92
+ * otherwise takes precedence over renderCharacter and escaped plain text.
80
93
  */
81
94
  renderText?: (text: string) => string;
82
95
  /**
@@ -108,8 +121,8 @@ export type OptionalTypewriterTextProperties = {
108
121
  onprogress?: (progress: TypewriterProgress) => void;
109
122
  /**
110
123
  * Render each revealed character yourself — highlight a token, wrap a number — instead
111
- * of the plain text node. Ignored when `renderText` is set, since that renderer already
112
- * owns the full markup. Falls back to plain text per character when omitted.
124
+ * of the plain text node. Ignored when markdown is true or renderText is set, since
125
+ * those renderers own the full markup. Falls back to plain text when omitted.
113
126
  */
114
127
  renderCharacter?: Snippet<[TypewriterCharacterContext]>;
115
128
  testId?: string;
@@ -87,6 +87,18 @@
87
87
  <style>
88
88
  .chart-container {
89
89
  width: 100%;
90
+ /* Floor against the silent zero-collapse bug: a flex/grid ancestor that
91
+ sizes itself by shrink-to-fit/fit-content resolves this div's
92
+ `width: 100%` as an indefinite percentage and contributes 0 to that
93
+ calculation, so the ancestor -- and this div -- settle at 0px with no
94
+ signal, and the `{#if width > 0 && height > 0}` guard below then
95
+ renders nothing at all. A definite min-width isn't subject to that;
96
+ it gives the ancestor a real, non-zero size to lay out against, so
97
+ the collapse can't happen in the first place. Overridable per
98
+ instance via --chart-min-width; PieChart's legend-right layout
99
+ already sets this to 0 for its own case, where flex-basis is
100
+ definite (0, not auto) so shrink-to-fit never applies. */
101
+ min-width: var(--chart-min-width, 160px);
90
102
  background: var(--chart-background, transparent);
91
103
  font-family: var(--chart-font-family, inherit);
92
104
  }
@@ -39,12 +39,26 @@
39
39
  });
40
40
 
41
41
  /**
42
- * Svelte action: relocates the tooltip to document.body so a position:fixed
42
+ * Where a portalled node is allowed to land. Svelte scopes a custom element's
43
+ * CSS to its shadow root, so `document.body` is the one place the tooltip must
44
+ * never go from inside a `<sui-*>` element: it keeps its `svelte-*` scoping
45
+ * class and loses every rule behind it, plus the host's custom properties.
46
+ * Measured in Chromium -- position fell back to `static`, background to
47
+ * transparent, border and shadow to none. Its own root holds that stylesheet
48
+ * and still sits above every overflow/scroll ancestor between the two.
49
+ */
50
+ const portalTarget = (node: Node): Node => {
51
+ const root = node.getRootNode();
52
+ return root instanceof ShadowRoot ? root : document.body;
53
+ };
54
+
55
+ /**
56
+ * Svelte action: relocates the tooltip out to its root so a position:fixed
43
57
  * tooltip is never clipped by an overflow/scroll ancestor. `use:` actions
44
58
  * never run during SSR.
45
59
  */
46
- const portalToBody = (node: HTMLElement) => {
47
- document.body.appendChild(node);
60
+ const portalToRoot = (node: HTMLElement) => {
61
+ portalTarget(node).appendChild(node);
48
62
  return { destroy: () => node.remove() };
49
63
  };
50
64
 
@@ -105,7 +119,7 @@
105
119
  bind:clientHeight={tooltipHeight}
106
120
  class="chart-tooltip portal {unstyled ? 'unstyled' : ''} {classes ?? ''}"
107
121
  style="left: {pos.left}px; top: {pos.top}px;"
108
- use:portalToBody
122
+ use:portalToRoot
109
123
  data-pw="chart-tooltip"
110
124
  testID="chart-tooltip"
111
125
  >
@@ -1,3 +1,24 @@
1
+ <script module lang="ts">
2
+ /**
3
+ * Resolves a caller-stable id (a category label, series name, or any other id
4
+ * the CALLER chose to remember) to its CURRENT position in a chart's live
5
+ * category list -- e.g. the array `ChartHighlightAPI.getCategories()` returns.
6
+ *
7
+ * Both `highlightedIndex` and `ChartHighlightAPI.highlight()` take a numeric
8
+ * index, but that index shifts whenever the chart reorders, filters, or
9
+ * folds tail categories into a `topN` "Other" bucket. A synchronized legend
10
+ * (or any external driver) should not cache the index it first saw; it
11
+ * should re-resolve the id against the current `getCategories()` output
12
+ * right before calling `highlight()`. Returns `null` when the id is not
13
+ * currently present (filtered out, or absorbed into an aggregate bucket),
14
+ * which callers should treat as "clear the highlight", not "highlight -1".
15
+ */
16
+ export function resolveLegendIndex(categories: readonly string[], id: string): number | null {
17
+ const index = categories.indexOf(id);
18
+ return index === -1 ? null : index;
19
+ }
20
+ </script>
21
+
1
22
  <script lang="ts">
2
23
  import type { LegendProperties } from './types';
3
24
 
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Resolves a caller-stable id (a category label, series name, or any other id
3
+ * the CALLER chose to remember) to its CURRENT position in a chart's live
4
+ * category list -- e.g. the array `ChartHighlightAPI.getCategories()` returns.
5
+ *
6
+ * Both `highlightedIndex` and `ChartHighlightAPI.highlight()` take a numeric
7
+ * index, but that index shifts whenever the chart reorders, filters, or
8
+ * folds tail categories into a `topN` "Other" bucket. A synchronized legend
9
+ * (or any external driver) should not cache the index it first saw; it
10
+ * should re-resolve the id against the current `getCategories()` output
11
+ * right before calling `highlight()`. Returns `null` when the id is not
12
+ * currently present (filtered out, or absorbed into an aggregate bucket),
13
+ * which callers should treat as "clear the highlight", not "highlight -1".
14
+ */
15
+ export declare function resolveLegendIndex(categories: readonly string[], id: string): number | null;
1
16
  import type { LegendProperties } from './types';
2
17
  declare const Legend: import("svelte").Component<LegendProperties, {}, "">;
3
18
  type Legend = ReturnType<typeof Legend>;
@@ -30,6 +30,7 @@ export type AutoLayout = ChartDimensions & {
30
30
  * then bottom derives from the rotation outcome.
31
31
  */
32
32
  export declare function computeAutoLayout(input: AutoLayoutInput): AutoLayout;
33
+ export declare const pieSliceValue: (value: number) => number;
33
34
  export declare function computePieLayout(data: Array<{
34
35
  label: string;
35
36
  value: number;
@@ -39,7 +40,7 @@ export declare function computeSankeyLayout(nodes: Array<{
39
40
  id: string;
40
41
  label?: string;
41
42
  color?: string;
42
- }>, links: Array<{
43
+ }>, rawLinks: Array<{
43
44
  source: string;
44
45
  target: string;
45
46
  value: number;
@@ -48,6 +49,58 @@ export declare function computeSankeyLayout(nodes: Array<{
48
49
  nodes: ComputedSankeyNode[];
49
50
  links: ComputedSankeyLink[];
50
51
  };
52
+ /**
53
+ * Minimal shape `joinByX` needs from a data point. `label` stays optional so
54
+ * both `LineChartDataPoint` and `AreaChartDataPoint` satisfy it structurally
55
+ * without either chart importing the other's types.
56
+ */
57
+ export type JoinableXPoint = {
58
+ x: number;
59
+ y: number;
60
+ label?: string;
61
+ };
62
+ export type JoinableXSeries<P extends JoinableXPoint = JoinableXPoint> = {
63
+ data: ReadonlyArray<P>;
64
+ };
65
+ export type JoinedXValue<P extends JoinableXPoint = JoinableXPoint> = {
66
+ /** Index of `point` within that series' own `data` array (last-wins, see below). */
67
+ index: number;
68
+ point: P;
69
+ };
70
+ export type JoinedXRow<P extends JoinableXPoint = JoinableXPoint> = {
71
+ x: number;
72
+ /**
73
+ * One slot per input series, in the same order as `seriesList`. `null`
74
+ * marks a series with no sample at this x -- an absence, never a fabricated
75
+ * zero. A gap point (finite x, non-finite y) still gets a slot here; callers
76
+ * that must skip gaps check `Number.isFinite(value.point.y)` themselves.
77
+ */
78
+ values: ReadonlyArray<JoinedXValue<P> | null>;
79
+ };
80
+ /**
81
+ * Aligns multiple series by their x VALUE instead of by array position, so a
82
+ * shorter, offset, reordered or duplicate-x series never has another
83
+ * series' sample attributed to its column (array-position joins silently
84
+ * draw series B's 3rd point at series A's 3rd x instead of B's own x).
85
+ *
86
+ * One row is produced per distinct finite x across all series, sorted
87
+ * ascending -- callers do not need to pre-sort their data. Non-finite x
88
+ * (an unusual, distinct case from a gap's non-finite Y) cannot be placed in
89
+ * a column at all and is excluded from the join.
90
+ *
91
+ * Duplicate x within a single series: the LAST matching point wins. This
92
+ * matches `computeStackedValues` below (which is built on this same join)
93
+ * and avoids privileging an arbitrary "first sample" when a caller's array
94
+ * isn't time-ordered.
95
+ *
96
+ * This is the ONE alignment contract shared by tooltip context, hit-testing
97
+ * and stacking/normalization -- there is no second, position-based path for
98
+ * the common case where every series already shares one x sequence: with
99
+ * aligned input this produces exactly the positional pairing callers relied
100
+ * on before, because "nearest by x" and "same index" agree when the x's
101
+ * already match one-for-one.
102
+ */
103
+ export declare function joinByX<P extends JoinableXPoint>(seriesList: ReadonlyArray<JoinableXSeries<P>>): JoinedXRow<P>[];
51
104
  export declare function computeStackedValues(seriesData: Array<Array<{
52
105
  x: number;
53
106
  y: number;
@@ -64,8 +64,19 @@ export function computeAutoLayout(input) {
64
64
  return { ...dims, xRotate: rotate, xEvery: every, xLabelOffset: xLabelDepth + TITLE_BAND };
65
65
  }
66
66
  // ── Pie layout ──────────────────────────────────────────────────
67
+ // Pie/funnel/sankey are value-accumulating charts: per docs/CHART_INPUT_POLICY.md
68
+ // a negative contribution has no visual meaning and is a 0 contribution, and
69
+ // (per computeStackedValues' precedent for this file) the same treatment is
70
+ // extended to non-finite values here rather than excluding them as a gap --
71
+ // unlike LineChart/AreaChart, a pie slice has no x-position to drop and
72
+ // omitting a data row would shift every later slice's array index instead of
73
+ // just rendering that one slice as zero-size. Exported so PieChart.svelte's
74
+ // independent total (feeds isEmpty and the percentage denominator) can't drift
75
+ // from this one -- two copies of an unguarded reduce is exactly how the
76
+ // original defect stayed invisible in half the places it needed the guard.
77
+ export const pieSliceValue = (value) => Number.isFinite(value) && value > 0 ? value : 0;
67
78
  export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0) {
68
- const total = data.reduce((sum, d) => sum + Math.max(0, d.value), 0);
79
+ const total = data.reduce((sum, d) => sum + pieSliceValue(d.value), 0);
69
80
  if (total === 0) {
70
81
  return [];
71
82
  }
@@ -73,7 +84,7 @@ export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0)
73
84
  let angle = startAngle;
74
85
  for (let i = 0; i < data.length; i++) {
75
86
  const d = data[i];
76
- const val = Math.max(0, d.value);
87
+ const val = pieSliceValue(d.value);
77
88
  const sliceAngle = (val / total) * Math.PI * 2;
78
89
  const start = angle + padAngle / 2;
79
90
  const end = angle + sliceAngle - padAngle / 2;
@@ -92,10 +103,22 @@ export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0)
92
103
  return slices;
93
104
  }
94
105
  // ── Sankey layout ───────────────────────────────────────────────
95
- export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16, nodePadding = 8, iterations = 6, minLinkWidth = 1) {
106
+ export function computeSankeyLayout(nodes, rawLinks, width, height, nodeWidth = 16, nodePadding = 8, iterations = 6, minLinkWidth = 1) {
96
107
  if (nodes.length === 0) {
97
108
  return { nodes: [], links: [] };
98
109
  }
110
+ // Single choke point: every downstream read of a link's value -- adjacency,
111
+ // node heights, the relaxation loop's weighted centring, link widths, and
112
+ // the returned links themselves -- goes through this sanitized array, not
113
+ // the raw prop. A non-finite value that reached any one of those sites was
114
+ // enough to broadcast NaN to the whole diagram (the per-column recentre
115
+ // step below spreads one node's NaN to every node sharing its column, and
116
+ // the relaxation loop then carries that into every downstream column too),
117
+ // so guarding only the site that happened to be probed was not sufficient.
118
+ // Zero-contribution, not gap-exclusion: matches computeStackedValues'
119
+ // precedent for this file (see pieSliceValue above) since a link has no
120
+ // x-position to drop the way a LineChart/AreaChart gap point does.
121
+ const links = rawLinks.map((l) => Number.isFinite(l.value) && l.value >= 0 ? l : { ...l, value: 0 });
99
122
  // Build adjacency
100
123
  const outgoing = new Map();
101
124
  const incoming = new Map();
@@ -161,7 +184,12 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
161
184
  }
162
185
  }
163
186
  if (!Number.isFinite(pxPerValue)) {
164
- // All-zero data: nothing carries volume, every bar collapses to the minimum.
187
+ // Nothing carries volume in any column, so every bar collapses to the
188
+ // minimum. Two different inputs land here: genuinely all-zero data, and
189
+ // a graph where every link was non-finite/negative and sanitized to 0
190
+ // above -- both reduce to the same "no volume anywhere" state, and the
191
+ // same minimum-width fallback is the correct degenerate render for
192
+ // either one (never NaN), so they do not need to stay distinguishable.
165
193
  pxPerValue = 0;
166
194
  }
167
195
  const linkRenderWidth = (value) => Math.max(minLinkWidth, value * pxPerValue);
@@ -263,7 +291,15 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
263
291
  column: columns.get(n.id) ?? 0
264
292
  }));
265
293
  const nodeById = new Map(computedNodes.map((n) => [n.id, n]));
266
- const linkKey = (l) => `${l.source}${l.target}`;
294
+ // The separator is written as the ESCAPE `\u0000`, not as a raw NUL byte. A
295
+ // literal NUL in the source makes the whole file binary to every text tool:
296
+ // `file` reports it as data, `grep` stops reporting matches in it, and a
297
+ // review diff renders it as unviewable. This file carried one from the commit
298
+ // that added the Sankey layout, and it silently hid `computeStackedValues`
299
+ // from a repo-wide grep. The runtime string is identical either way -- a NUL
300
+ // is still the delimiter, because it is the one character a node id cannot
301
+ // contain, which is the whole reason it was chosen.
302
+ const linkKey = (l) => `${l.source}\u0000${l.target}`;
267
303
  // Link widths use the same global scale as node heights, so each node's link
268
304
  // stack fills its bar exactly and never runs past its bottom edge.
269
305
  const linkWidths = new Map();
@@ -327,21 +363,93 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
327
363
  });
328
364
  return { nodes: computedNodes, links: computedLinks };
329
365
  }
366
+ /**
367
+ * Aligns multiple series by their x VALUE instead of by array position, so a
368
+ * shorter, offset, reordered or duplicate-x series never has another
369
+ * series' sample attributed to its column (array-position joins silently
370
+ * draw series B's 3rd point at series A's 3rd x instead of B's own x).
371
+ *
372
+ * One row is produced per distinct finite x across all series, sorted
373
+ * ascending -- callers do not need to pre-sort their data. Non-finite x
374
+ * (an unusual, distinct case from a gap's non-finite Y) cannot be placed in
375
+ * a column at all and is excluded from the join.
376
+ *
377
+ * Duplicate x within a single series: the LAST matching point wins. This
378
+ * matches `computeStackedValues` below (which is built on this same join)
379
+ * and avoids privileging an arbitrary "first sample" when a caller's array
380
+ * isn't time-ordered.
381
+ *
382
+ * This is the ONE alignment contract shared by tooltip context, hit-testing
383
+ * and stacking/normalization -- there is no second, position-based path for
384
+ * the common case where every series already shares one x sequence: with
385
+ * aligned input this produces exactly the positional pairing callers relied
386
+ * on before, because "nearest by x" and "same index" agree when the x's
387
+ * already match one-for-one.
388
+ */
389
+ export function joinByX(seriesList) {
390
+ const perSeriesByX = seriesList.map((series) => {
391
+ const byX = new Map();
392
+ series.data.forEach((point, index) => {
393
+ if (Number.isFinite(point.x)) {
394
+ byX.set(point.x, { index, point });
395
+ }
396
+ });
397
+ return byX;
398
+ });
399
+ const allX = new Set();
400
+ for (const byX of perSeriesByX) {
401
+ for (const x of byX.keys()) {
402
+ allX.add(x);
403
+ }
404
+ }
405
+ return [...allX]
406
+ .sort((a, b) => a - b)
407
+ .map((x) => ({
408
+ x,
409
+ values: perSeriesByX.map((byX) => byX.get(x) ?? null)
410
+ }));
411
+ }
330
412
  // ── Stacked values ──────────────────────────────────────────────
331
413
  export function computeStackedValues(seriesData) {
332
414
  if (seriesData.length === 0) {
333
415
  return [];
334
416
  }
335
- const stacked = [];
336
- const baselines = new Map();
337
- for (const series of seriesData) {
338
- const stackedSeries = [];
339
- for (const point of series) {
340
- const base = baselines.get(point.x) ?? 0;
341
- stackedSeries.push({ x: point.x, y0: base, y1: base + point.y });
342
- baselines.set(point.x, base + point.y);
417
+ // Built on joinByX -- the same by-x alignment used for tooltip/hit-testing
418
+ // -- rather than a second, independent accumulation. A series absent at a
419
+ // given x (including a NaN-gap point there) contributes no segment and no
420
+ // baseline shift at that column, instead of a fabricated zero-height slice
421
+ // or a NaN that would poison every series stacked above it.
422
+ const rows = joinByX(seriesData.map((data) => ({ data })));
423
+ const stacked = seriesData.map(() => []);
424
+ for (const row of rows) {
425
+ let base = 0;
426
+ for (let si = 0; si < seriesData.length; si++) {
427
+ const entry = row.values[si];
428
+ if (entry === null || !Number.isFinite(entry.point.y)) {
429
+ continue;
430
+ }
431
+ // A negative contributes ZERO height, it does not subtract.
432
+ //
433
+ // docs/CHART_INPUT_POLICY.md has always said so -- "a negative
434
+ // contribution to a stack or a percent-of-total does not have a
435
+ // well-defined visual meaning (a slice cannot have negative height), so it
436
+ // is treated as a 0 contribution rather than inverting the stack" -- and
437
+ // the clamp existed only in AreaChart's normalized path, behind
438
+ // `stackNormalize`. Plain `stacked: true` renders through here, where
439
+ // `base + y` let a negative REDUCE the baseline for every series above it.
440
+ //
441
+ // It was invisible rather than merely wrong: AreaChart's stacked y-domain
442
+ // starts at 0, so the inverted segment had nowhere to be drawn and landed
443
+ // at pixel 510 of a 340px plot -- off the bottom of the chart entirely.
444
+ //
445
+ // The segment is still pushed, with zero height, rather than skipped:
446
+ // skipping means "this series has no point at this x", which is what a gap
447
+ // already means, and would silently drop the series from the column.
448
+ const contribution = Math.max(0, entry.point.y);
449
+ const y1 = base + contribution;
450
+ stacked[si].push({ x: row.x, y0: base, y1 });
451
+ base = y1;
343
452
  }
344
- stacked.push(stackedSeries);
345
453
  }
346
454
  return stacked;
347
455
  }
@@ -8,8 +8,12 @@ export type RelativePointerPosition = {
8
8
  /**
9
9
  * Pointer position relative to the top-left corner of `el`, or null when the
10
10
  * element is not mounted yet.
11
+ *
12
+ * Takes `MouseEvent` (which `PointerEvent` extends) since it only reads
13
+ * clientX/clientY — that also lets the mouse-only chart components (PieChart,
14
+ * SankeyChart) share this instead of hand-rolling the same formula.
11
15
  */
12
- export declare function pointerPositionIn(el: HTMLElement | null, event: PointerEvent): RelativePointerPosition | null;
16
+ export declare function pointerPositionIn(el: HTMLElement | null, event: MouseEvent): RelativePointerPosition | null;
13
17
  /**
14
18
  * Touch taps never fire pointerleave, so a tap-opened tooltip would otherwise
15
19
  * stay stuck: dismiss when a pointerdown lands outside `containerEl`.
@@ -1,9 +1,11 @@
1
- /**
2
- * Shared pointer-interaction helpers used by the chart components.
3
- */
1
+ import { eventHitsInside } from '../_interaction/dismissal';
4
2
  /**
5
3
  * Pointer position relative to the top-left corner of `el`, or null when the
6
4
  * element is not mounted yet.
5
+ *
6
+ * Takes `MouseEvent` (which `PointerEvent` extends) since it only reads
7
+ * clientX/clientY — that also lets the mouse-only chart components (PieChart,
8
+ * SankeyChart) share this instead of hand-rolling the same formula.
7
9
  */
8
10
  export function pointerPositionIn(el, event) {
9
11
  if (el === null) {
@@ -24,8 +26,10 @@ export function dismissOnOutsidePointerDown(containerEl, onDismiss) {
24
26
  return () => { };
25
27
  }
26
28
  const dismiss = (event) => {
27
- const target = event.target;
28
- if (containerEl !== null && !(target instanceof Node && containerEl.contains(target))) {
29
+ // Through the custom-element build this listener sits outside the shadow
30
+ // root the chart renders in, so `event.target` is the host rather than
31
+ // anything the pointer actually hit. See eventHitsInside.
32
+ if (containerEl !== null && !eventHitsInside(containerEl, event)) {
29
33
  onDismiss();
30
34
  }
31
35
  };
@@ -1,4 +1,20 @@
1
1
  export function niceLinearDomain(min, max) {
2
+ // Defense-in-depth: every current caller already filters to finite values
3
+ // before reaching here (LineChart/AreaChart/BarChart/DualAxisBarChart), so
4
+ // this never fires for them. It exists so a future caller that forgets to
5
+ // filter degrades to a neutral domain instead of propagating NaN through
6
+ // every downstream tick and pixel computation (min === max never catches
7
+ // NaN, since NaN === NaN is false).
8
+ //
9
+ // It also MASKS those callers' own filters, which matters when testing them:
10
+ // remove a chart's finite-filter and the axis no longer loses its ticks, it
11
+ // silently scales to [0, 1] instead. So a `ticks.length > 0` assertion is
12
+ // vacuous as a test of a caller-side guard — it passes either way. Assert on
13
+ // tick *content* against a reference render (see BarChart/non-finite.test.ts)
14
+ // instead.
15
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
16
+ return [0, 1];
17
+ }
2
18
  if (min === max) {
3
19
  return min === 0 ? [0, 1] : [min > 0 ? 0 : min * 2, max > 0 ? max * 2 : 0];
4
20
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Documented, tested calendar-tick helpers for the temporal limitation in
3
+ * Line/AreaChart's x axis -- see docs/CHART_INPUT_POLICY.md#calendar--temporal-axes
4
+ * for the full policy this satisfies.
5
+ *
6
+ * Line/AreaChart's x axis is a plain numeric linear scale (`createLinearScale`
7
+ * in `./scales`). Epoch-millisecond x values preserve relative distance --
8
+ * `t2 - t1` still means the same thing in pixels as it does in time -- but
9
+ * `computeLinearTicks`'s "nice round number" ticks are not calendar-aware:
10
+ * they land on values like 1_700_000_000_000 or a rounded step in
11
+ * milliseconds, not on UTC midnight/week/month boundaries. `xTickFormat`
12
+ * only changes how a tick's existing numeric position is labelled; it cannot
13
+ * move that position onto a calendar boundary, and neither chart accepts an
14
+ * explicit tick-position override today (`Axis.svelte`, shared by every chart
15
+ * type in this package, always derives its own tick positions from the
16
+ * scale) -- wiring one in is deliberately out of scope here, since it would
17
+ * change a contract every other chart consumes, not just these two.
18
+ *
19
+ * `computeUtcTicks`/`formatUtcTick` are the disclosed alternative: pure,
20
+ * fully-tested functions a caller can use to compute correct UTC calendar
21
+ * tick positions and labels for their own presentation (a custom axis
22
+ * overlay, a legend of period boundaries, or pre-bucketing data into
23
+ * calendar-aligned x values before it ever reaches the chart). They do not
24
+ * replace the chart's linear scale with array indices, and they impose
25
+ * nothing on a caller who does not import this module -- both charts behave
26
+ * exactly as before.
27
+ */
28
+ export type UtcTickUnit = 'day' | 'week' | 'month';
29
+ /**
30
+ * Computes UTC calendar-aligned tick positions (epoch ms) covering
31
+ * `[domain[0], domain[1]]`. `unit` picks the calendar grain; ticks are
32
+ * thinned so the count stays at or under `maxTicks` (mirrors Line/AreaChart's
33
+ * own tick-count capping).
34
+ *
35
+ * Returns `[]` for a non-finite or inverted domain -- callers pass this
36
+ * straight into `xTicks`, and an empty array there just falls back to no
37
+ * override (see LineChart/AreaChart docs).
38
+ */
39
+ export declare function computeUtcTicks(domain: readonly [number, number], unit: UtcTickUnit, maxTicks?: number): number[];
40
+ /**
41
+ * Formats an epoch-ms tick value produced by `computeUtcTicks` as a UTC
42
+ * calendar label ("Mar 4" for day/week, "Mar 2026" for month). Pass as
43
+ * `xTickFormat` alongside `xTicks={computeUtcTicks(...)}`.
44
+ */
45
+ export declare function formatUtcTick(value: number, unit?: UtcTickUnit): string;