@juspay/svelte-ui-components 2.80.5 → 2.80.7

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.
@@ -18,7 +18,7 @@
18
18
  import { formatNumber } from '../_chart/format';
19
19
  import { roundedRectPath } from '../_chart/paths';
20
20
  import type { LegendItem, BarRect } from '../_chart/types';
21
- import { DEFAULT_CHART_CORNER_RADIUS } from '../_chart/types';
21
+ import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
22
22
  import { SvelteMap } from 'svelte/reactivity';
23
23
 
24
24
  // ── Per-instance uid prefix for <defs> ids (A1-3) ─────────────
@@ -55,6 +55,8 @@
55
55
  barPadding = 0.2,
56
56
  barRadius = DEFAULT_CHART_CORNER_RADIUS,
57
57
  aspectRatio = 16 / 9,
58
+ maxHeight = DEFAULT_CHART_MAX_HEIGHT,
59
+ minHeight = 0,
58
60
  xAxisLabel,
59
61
  yAxisLabel,
60
62
  yDomain,
@@ -108,7 +110,17 @@
108
110
 
109
111
  let format = $derived(valueFormat ?? formatNumber);
110
112
  let isVertical = $derived(orientation === 'vertical');
111
- let dims = $derived(computeChartDimensions(chartWidth, chartHeight));
113
+ // When an axis is hidden its gutter (Y = 50px for tick labels, X = 40px) is dead
114
+ // space that squeezes the plot into the centre. Collapse the tick-label gutter but
115
+ // keep a symmetric breathing-room inset so the edge bars (and their value labels)
116
+ // don't sit flush against the container edges.
117
+ let dims = $derived(
118
+ computeChartDimensions(chartWidth, chartHeight, {
119
+ left: showYAxis ? 50 : 28,
120
+ right: 28,
121
+ bottom: showXAxis ? 40 : 8
122
+ })
123
+ );
112
124
 
113
125
  let isMulti = $derived(Array.isArray(series) && series.length > 0);
114
126
 
@@ -606,7 +618,13 @@
606
618
  : ''}
607
619
  >
608
620
  <div style={scrollable ? `min-width: ${minScrollWidth}px;` : ''}>
609
- <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
621
+ <ChartContainer
622
+ bind:width={chartWidth}
623
+ bind:height={chartHeight}
624
+ {aspectRatio}
625
+ {maxHeight}
626
+ {minHeight}
627
+ >
610
628
  <!-- A1-2 / A1-3: SVG <defs> for pattern and gradient fills -->
611
629
  {#if defsEntries.length > 0}
612
630
  <defs>
@@ -817,7 +835,8 @@
817
835
  }
818
836
  .bar-value {
819
837
  fill: var(--barchart-value-color, #333);
820
- font-size: var(--barchart-value-font-size, 11px);
838
+ font-size: var(--barchart-value-font-size, 14px);
839
+ font-weight: var(--barchart-value-font-weight, 600);
821
840
  font-family: var(--chart-font-family, inherit);
822
841
  pointer-events: none;
823
842
  }
@@ -84,6 +84,14 @@ export type OptionalBarChartProperties = {
84
84
  */
85
85
  barRadius?: number;
86
86
  aspectRatio?: number;
87
+ /**
88
+ * Upper bound (px) on the rendered chart height. The chart height is derived from
89
+ * its width via `aspectRatio`; on wide or scrollable surfaces that can balloon the
90
+ * chart. Cap it here to keep the chart compact (defaults to `Infinity` = uncapped).
91
+ */
92
+ maxHeight?: number;
93
+ /** Lower bound (px) on the rendered chart height (defaults to `0`). */
94
+ minHeight?: number;
87
95
  xAxisLabel?: string;
88
96
  yAxisLabel?: string;
89
97
  yDomain?: [number, number];
@@ -213,6 +213,11 @@
213
213
  gap: var(--button-content-gap, 16px);
214
214
  visibility: var(--button-visibility, visible);
215
215
  box-shadow: var(--button-box-shadow, none);
216
+
217
+ /* A button label should never soft-wrap onto a second line as the label text
218
+ changes (e.g. "Select" -> "Select (1)") — that jitters the layout around it.
219
+ Consumers that genuinely want multi-line buttons opt back in via the var. */
220
+ white-space: var(--button-white-space, nowrap);
216
221
  }
217
222
 
218
223
  .button-el.disabled,
@@ -68,6 +68,14 @@
68
68
  color: var(--delta-indicator-neutral-color, #8a8a8a);
69
69
  }
70
70
 
71
+ /* The tone color is set on the wrapper; the text span must inherit it explicitly.
72
+ A consumer's global `span { color }` rule (higher weight than inheritance) would
73
+ otherwise repaint just the number a neutral text color — leaving only the arrow
74
+ tinted and making up/down deltas read as colorless. */
75
+ .delta-indicator-text {
76
+ color: inherit;
77
+ }
78
+
71
79
  .delta-indicator-arrow {
72
80
  display: inline-flex;
73
81
  width: var(--delta-indicator-arrow-size, 9px);
@@ -14,7 +14,7 @@
14
14
  import { formatNumber } from '../_chart/format';
15
15
  import { roundedRectPath, linePath } from '../_chart/paths';
16
16
  import type { LegendItem, TooltipData, LinearScale, BandScale, Point } from '../_chart/types';
17
- import { DEFAULT_CHART_CORNER_RADIUS } from '../_chart/types';
17
+ import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
18
18
 
19
19
  // ── Per-instance uid for SVG <defs> ids ────────────────────────
20
20
  const uid = Math.random().toString(36).slice(2, 9);
@@ -31,6 +31,8 @@
31
31
  barRadius = DEFAULT_CHART_CORNER_RADIUS,
32
32
  barPadding = 0.25,
33
33
  aspectRatio = 16 / 9,
34
+ maxHeight = DEFAULT_CHART_MAX_HEIGHT,
35
+ minHeight = 0,
34
36
  minBarHeight = 2,
35
37
  margin,
36
38
  tooltipPortal = false,
@@ -341,7 +343,13 @@
341
343
  <Legend items={legendItems} position="top" />
342
344
  {/if}
343
345
 
344
- <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
346
+ <ChartContainer
347
+ bind:width={chartWidth}
348
+ bind:height={chartHeight}
349
+ {aspectRatio}
350
+ {maxHeight}
351
+ {minHeight}
352
+ >
345
353
  <g transform="translate({dims.margin.left}, {dims.margin.top})">
346
354
  <!-- Left Y-axis (index 0) -->
347
355
  <Axis
@@ -91,6 +91,13 @@ export type OptionalDualAxisBarChartProperties = {
91
91
  * Passed to `ChartContainer`'s ResizeObserver sizing. Default `16/9`.
92
92
  */
93
93
  aspectRatio?: number;
94
+ /**
95
+ * Upper bound (px) on the rendered chart height, so the aspect-ratio-derived height
96
+ * can't balloon on wide surfaces. Defaults to `Infinity` (uncapped).
97
+ */
98
+ maxHeight?: number;
99
+ /** Lower bound (px) on the rendered chart height (defaults to `0`). */
100
+ minHeight?: number;
94
101
  /**
95
102
  * Custom tooltip content. Receives a `DualAxisTooltipContext` and replaces the
96
103
  * default multi-series tooltip.
@@ -4,7 +4,7 @@
4
4
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
5
5
  import { getColor } from '../_chart/colors';
6
6
  import { formatNumber, formatPercent } from '../_chart/format';
7
- import { DEFAULT_CHART_CORNER_RADIUS } from '../_chart/types';
7
+ import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
8
8
 
9
9
  // ── Props ──────────────────────────────────────────────────────
10
10
 
@@ -17,6 +17,8 @@
17
17
  showValueLabels = true,
18
18
  valueFormat,
19
19
  aspectRatio = 16 / 9,
20
+ maxHeight = DEFAULT_CHART_MAX_HEIGHT,
21
+ minHeight = 0,
20
22
  radius = DEFAULT_CHART_CORNER_RADIUS,
21
23
  testId,
22
24
  classes,
@@ -219,7 +221,13 @@
219
221
  {#if isEmpty && typeof empty === 'function'}
220
222
  <div class="chart-empty">{@render empty()}</div>
221
223
  {:else}
222
- <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
224
+ <ChartContainer
225
+ bind:width={chartWidth}
226
+ bind:height={chartHeight}
227
+ {aspectRatio}
228
+ {maxHeight}
229
+ {minHeight}
230
+ >
223
231
  <g transform="translate({MARGIN_LEFT}, {MARGIN_TOP})">
224
232
  <!-- Stage bars and category labels -->
225
233
  {#each data as stage, index (index)}
@@ -55,6 +55,13 @@ export type OptionalFunnelChartProperties = {
55
55
  * Passed directly to `ChartContainer`. Default is `16 / 9`.
56
56
  */
57
57
  aspectRatio?: number;
58
+ /**
59
+ * Upper bound (px) on the rendered chart height, so the aspect-ratio-derived height
60
+ * can't balloon on wide surfaces. Defaults to `Infinity` (uncapped).
61
+ */
62
+ maxHeight?: number;
63
+ /** Lower bound (px) on the rendered chart height (defaults to `0`). */
64
+ minHeight?: number;
58
65
  /** Value for the `data-pw` attribute on the chart root element. */
59
66
  testId?: string;
60
67
  /** CSS class string applied to the chart root element. Useful for CSS-variable theming. */
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import Img from '../Img/Img.svelte';
2
3
  import type { IconProperties } from './properties';
3
4
 
4
5
  let { icon, svg, text, onclick, onkeydown, classes }: IconProperties = $props();
@@ -9,7 +10,9 @@
9
10
  <!-- eslint-disable svelte/no-at-html-tags -->
10
11
  <span class="icon-svg">{@html svg}</span>
11
12
  {:else if icon}
12
- <img src={icon} alt="" />
13
+ <!-- Inline the SVG so currentColor strokes/fills inherit the surrounding text
14
+ colour; non-SVG URLs fall back to the plain <img> render automatically. -->
15
+ <Img inlineSvg src={icon} alt="" fallback="" />
13
16
  {/if}
14
17
  {#if typeof text === 'string' && text.length > 0}
15
18
  <div class="icon-text">{text}</div>
@@ -24,7 +27,10 @@
24
27
  align-items: center;
25
28
  cursor: pointer;
26
29
  }
27
- .icon-container img {
30
+ /* Direct child only: the @html branch's svg lives inside .icon-svg and keeps
31
+ its own 100% sizing rule below. */
32
+ .icon-container > :global(img),
33
+ .icon-container > :global(svg) {
28
34
  height: var(--icon-height, 20px);
29
35
  width: var(--icon-width, 20px);
30
36
  padding: var(--icon-padding, 4px);
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { LineChartProperties, LineChartTooltipContext } from './properties';
3
+ import { DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
3
4
  import type { ChartHighlightAPI } from '../_chart/highlight';
4
5
  import { onMount } from 'svelte';
5
6
  import ChartContainer from '../_chart/ChartContainer.svelte';
@@ -45,7 +46,7 @@
45
46
  yTickFormat,
46
47
  aspectRatio = 16 / 9,
47
48
  minHeight = 0,
48
- maxHeight = Infinity,
49
+ maxHeight = DEFAULT_CHART_MAX_HEIGHT,
49
50
  tooltipSnippet,
50
51
  empty,
51
52
  highlightedIndex = null,
@@ -13,6 +13,7 @@
13
13
  onopen,
14
14
  onclose,
15
15
  classes,
16
+ selectedValue = null,
16
17
  role: menuRole = 'menu',
17
18
  ariaLabel: menuAriaLabel,
18
19
  id: menuId
@@ -43,12 +44,20 @@
43
44
  }
44
45
  }
45
46
 
46
- function openMenu(startIndex: number = 0) {
47
+ function openMenu(startIndex: number | null = null) {
47
48
  open = true;
48
- focusedIndex = startIndex;
49
+ // With a known selection, opening focuses the selected option (listbox
50
+ // convention) instead of always parking the focus highlight on item 0.
51
+ const selectedItem =
52
+ selectedValue != null
53
+ ? (items.find((item) => item.value === selectedValue && item.disabled !== true) ?? null)
54
+ : null;
55
+ const initialIndex =
56
+ startIndex ?? (selectedItem !== null ? getSelectableIndex(selectedItem) : 0);
57
+ focusedIndex = initialIndex;
49
58
  onopen?.();
50
59
  tick().then(() => {
51
- focusItem(startIndex);
60
+ focusItem(initialIndex);
52
61
  });
53
62
  }
54
63
 
@@ -232,12 +241,17 @@
232
241
  class="menu-item"
233
242
  class:menu-item-danger={item.danger === true}
234
243
  class:menu-item-disabled={item.disabled === true}
244
+ class:menu-item-selected={selectedValue != null && item.value === selectedValue}
235
245
  role={itemRole}
236
246
  id={item.id}
237
247
  tabindex={item.disabled === true || menuRole === 'listbox' ? -1 : 0}
238
248
  aria-disabled={item.disabled === true ? 'true' : null}
239
- aria-selected={menuRole === 'listbox' && focusedIndex === getSelectableIndex(item)
240
- ? true
249
+ aria-selected={menuRole === 'listbox'
250
+ ? selectedValue != null
251
+ ? item.value === selectedValue
252
+ : focusedIndex === getSelectableIndex(item)
253
+ ? true
254
+ : null
241
255
  : null}
242
256
  onclick={() => selectItem(item)}
243
257
  onfocus={() => {
@@ -249,7 +263,10 @@
249
263
  >
250
264
  {#if typeof item.icon === 'string'}
251
265
  <span class="menu-item-icon">
252
- <Img src={item.icon} alt="" />
266
+ <!-- Inline the SVG so currentColor strokes/fills inherit the item's
267
+ text colour (danger items tint their icon red, dark theme works);
268
+ non-SVG URLs fall back to the plain <img> render automatically. -->
269
+ <Img inlineSvg src={item.icon} alt="" fallback="" />
253
270
  </span>
254
271
  {/if}
255
272
  <span class="menu-item-label">{item.label}</span>
@@ -315,6 +332,13 @@
315
332
  outline: var(--menu-item-focus-outline, none);
316
333
  }
317
334
 
335
+ /* True selection (selectedValue match) — distinct from transient focus so the
336
+ highlight follows the chosen option, not wherever keyboard focus landed. */
337
+ .menu-item-selected {
338
+ background-color: var(--menu-item-selected-background-color, transparent);
339
+ color: var(--menu-item-selected-color, inherit);
340
+ }
341
+
318
342
  .menu-item-danger {
319
343
  color: var(--menu-item-danger-color, #dc3545);
320
344
  }
@@ -347,7 +371,8 @@
347
371
  flex-shrink: 0;
348
372
  }
349
373
 
350
- .menu-item-icon :global(img) {
374
+ .menu-item-icon :global(img),
375
+ .menu-item-icon :global(svg) {
351
376
  width: 100%;
352
377
  height: 100%;
353
378
  }
@@ -17,6 +17,11 @@ export type OptionalMenuProperties = {
17
17
  testId?: string;
18
18
  trigger?: Snippet;
19
19
  classes?: string;
20
+ /** Value of the currently selected item. When set, opening the menu focuses the
21
+ * selected option instead of the first item, the matching item gets the
22
+ * `menu-item-selected` class (stylable via --menu-item-selected-*), and
23
+ * listbox aria-selected reflects the real selection. */
24
+ selectedValue?: string | null;
20
25
  role?: 'menu' | 'listbox';
21
26
  ariaLabel?: string;
22
27
  id?: string;
@@ -5,6 +5,7 @@
5
5
  import OverlayAnimation from '../Animations/OverlayAnimation.svelte';
6
6
  import { createDebouncer } from '../utils';
7
7
  import Button from '../Button/Button.svelte';
8
+ import Img from '../Img/Img.svelte';
8
9
 
9
10
  let overlayDiv: HTMLDivElement | null = $state(null);
10
11
  let backPressed = $state(false);
@@ -162,7 +163,15 @@
162
163
  tabindex="0"
163
164
  data-pw={leftImageTestId}
164
165
  >
165
- <img class="header-left-img" src={header.leftImage} alt="" />
166
+ <!-- Inline SVGs so currentColor icons inherit the header text
167
+ colour; non-SVG URLs fall back to a plain <img>. -->
168
+ <Img
169
+ inlineSvg
170
+ src={header.leftImage}
171
+ alt=""
172
+ fallback=""
173
+ classes="header-left-img"
174
+ />
166
175
  </div>
167
176
  {/if}
168
177
  {#if typeof header.text === 'string' && header.text.length > 0}
@@ -178,7 +187,13 @@
178
187
  onkeydown={handleRightImageKeyDown}
179
188
  data-pw={header.buttonTestId}
180
189
  >
181
- <img class="header-right-img" src={header.rightImage} alt="" />
190
+ <Img
191
+ inlineSvg
192
+ src={header.rightImage}
193
+ alt=""
194
+ fallback=""
195
+ classes="header-right-img"
196
+ />
182
197
  </div>
183
198
  {/if}
184
199
  </div>
@@ -409,19 +424,22 @@
409
424
  letter-spacing: var(--modal-header-text-letter-spacing);
410
425
  }
411
426
 
412
- .header-left-img,
413
- .header-right-img {
427
+ /* The header images render through the Img component (inline svg or img), so
428
+ these classes ride on Img's element — match them via :global under the
429
+ scoped .header to keep the sizing contract. */
430
+ .header :global(.header-left-img),
431
+ .header :global(.header-right-img) {
414
432
  padding-top: var(--header-img-top-padding, 5px);
415
433
  cursor: pointer;
416
434
  }
417
435
 
418
- .header-left-img {
436
+ .header :global(.header-left-img) {
419
437
  margin: var(--header-left-image-margin, 0px 18px 0px 0px);
420
438
  width: var(--header-left-image-width, 25px);
421
439
  height: var(--header-left-image-height, 25px);
422
440
  }
423
441
 
424
- .header-right-img {
442
+ .header :global(.header-right-img) {
425
443
  width: var(--header-right-image-width, 25px);
426
444
  height: var(--header-right-image-height, 25px);
427
445
  padding: var(--header-right-image-padding);
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
+ import { DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
3
4
  import type { PieChartProperties } from './properties';
4
5
  import ChartContainer from '../_chart/ChartContainer.svelte';
5
6
  import ChartTooltip from '../_chart/ChartTooltip.svelte';
@@ -23,6 +24,8 @@
23
24
  showLegend = false,
24
25
  startAngle = -Math.PI / 2,
25
26
  aspectRatio,
27
+ maxHeight = DEFAULT_CHART_MAX_HEIGHT,
28
+ minHeight = 0,
26
29
  valueFormat,
27
30
  tooltipSnippet,
28
31
  center,
@@ -221,6 +224,8 @@
221
224
  bind:width={chartWidth}
222
225
  bind:height={chartHeight}
223
226
  aspectRatio={effectiveAspectRatio}
227
+ {maxHeight}
228
+ {minHeight}
224
229
  >
225
230
  <g transform="translate({cx}, {cy})">
226
231
  {#each slices as slice (slice.index)}
@@ -18,6 +18,13 @@ export type OptionalPieChartProperties = {
18
18
  showLegend?: boolean;
19
19
  startAngle?: number;
20
20
  aspectRatio?: number;
21
+ /**
22
+ * Upper bound (px) on the rendered chart height, so the aspect-ratio-derived height
23
+ * can't balloon on wide surfaces. Defaults to `Infinity` (uncapped).
24
+ */
25
+ maxHeight?: number;
26
+ /** Lower bound (px) on the rendered chart height (defaults to `0`). */
27
+ minHeight?: number;
21
28
  valueFormat?: (value: number) => string;
22
29
  tooltipSnippet?: Snippet<[PieChartSlice, number]>;
23
30
  center?: Snippet;
@@ -5,7 +5,7 @@
5
5
  import { computeSankeyLayout } from '../_chart/geometry';
6
6
  import { getColor } from '../_chart/colors';
7
7
  import { formatNumber } from '../_chart/format';
8
- import { DEFAULT_CHART_CORNER_RADIUS } from '../_chart/types';
8
+ import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
9
9
  import { SvelteMap, SvelteSet } from 'svelte/reactivity';
10
10
 
11
11
  // ── Props ──────────────────────────────────────────────────────
@@ -20,7 +20,7 @@
20
20
  showLabels = true,
21
21
  aspectRatio = 16 / 9,
22
22
  radius = DEFAULT_CHART_CORNER_RADIUS,
23
- maxHeight = Infinity,
23
+ maxHeight = DEFAULT_CHART_MAX_HEIGHT,
24
24
  valueFormat,
25
25
  tooltipSnippet,
26
26
  empty,
@@ -52,11 +52,39 @@
52
52
  let format = $derived(valueFormat ?? formatNumber);
53
53
  let isEmpty = $derived(nodes.length === 0);
54
54
  const MARGIN = 40;
55
+ const LABEL_CHAR_PX = 7.2; // ≈ 0.6em at the 12px default label size
56
+
57
+ // Final-column labels render to the RIGHT of their node; the bare 40px margin is
58
+ // nowhere near enough for real funnel labels ("PARTIALLY_FAILED (1,234)"), so they
59
+ // used to run past the svg edge and clip. Reserve a capped gutter sized from the
60
+ // sink-node labels (sinks are what land in the final column) and lay the diagram
61
+ // out in the remaining width instead.
62
+ let lastColumnLabelGutter = $derived.by(() => {
63
+ if (!showLabels || nodes.length === 0 || chartWidth <= 0) {
64
+ return 0;
65
+ }
66
+ const sourceIds = new Set(links.map((link) => link.source));
67
+ const sinkLabels = nodes
68
+ .filter((node) => !sourceIds.has(node.id))
69
+ .map((node) => node.label ?? node.id);
70
+ if (sinkLabels.length === 0) {
71
+ return 0;
72
+ }
73
+ const longestChars =
74
+ Math.max(...sinkLabels.map((label) => label.length)) + (showValues ? 9 : 0);
75
+ const wanted = longestChars * LABEL_CHAR_PX + 10 + dataLabelOffsetX;
76
+ // Cap the reservation so labels can never squeeze the diagram below 3/4 width,
77
+ // and floor at 0 — a negative dataLabelOffsetX must not inflate the plot
78
+ // past the right margin.
79
+ return Math.max(0, Math.min(chartWidth * 0.25, wanted));
80
+ });
81
+
82
+ let plotWidth = $derived(Math.max(0, chartWidth - MARGIN * 2 - lastColumnLabelGutter));
55
83
  let layout = $derived(
56
84
  computeSankeyLayout(
57
85
  nodes,
58
86
  links,
59
- Math.max(0, chartWidth - MARGIN * 2),
87
+ plotWidth,
60
88
  Math.max(0, chartHeight - MARGIN * 2),
61
89
  nodeWidth,
62
90
  nodePadding,
@@ -87,21 +115,31 @@
87
115
  layout.nodes.length > 0 ? Math.max(...layout.nodes.map((n) => n.column)) + 1 : 0
88
116
  );
89
117
  let colWidth = $derived(
90
- columnCount <= 1
91
- ? Math.max(0, chartWidth - MARGIN * 2)
92
- : (Math.max(0, chartWidth - MARGIN * 2) - nodeWidth) / (columnCount - 1)
118
+ columnCount <= 1 ? plotWidth : (plotWidth - nodeWidth) / (columnCount - 1)
93
119
  );
94
120
 
95
121
  // Node labels render alongside their bar; long labels used to overflow into the
96
122
  // next column and collide. Clip each to the horizontal room its column actually
97
123
  // has and append an ellipsis (the full text stays available via the node tooltip).
98
- const LABEL_CHAR_PX = 7.2; // 0.6em at the 12px default label size
124
+ // The final column's room is the reserved right gutter (plus the margin), NOT
125
+ // colWidth — budgeting it at colWidth is what used to let edge labels clip.
126
+ // Column headers are centred over columns that narrow as the label gutter and
127
+ // column count grow; untruncated they collide into one unreadable run. Clip to
128
+ // the column pitch with an ellipsis — the full text stays on the <title>.
129
+ const truncateColumnLabel = (text: string): string => {
130
+ const maxChars = Math.floor(Math.max(0, colWidth - 6) / LABEL_CHAR_PX);
131
+ if (maxChars < 3) {
132
+ return '';
133
+ }
134
+ return text.length > maxChars ? text.slice(0, maxChars - 1) + '…' : text;
135
+ };
136
+
99
137
  const truncateLabel = (text: string, column: number): string => {
100
138
  const available =
101
139
  column === 0
102
140
  ? MARGIN + 16
103
141
  : column === columnCount - 1
104
- ? Math.max(colWidth, MARGIN + 24)
142
+ ? Math.max(0, lastColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
105
143
  : Math.max(0, colWidth - nodeWidth - 12);
106
144
  const maxChars = Math.floor(available / LABEL_CHAR_PX);
107
145
  // No usable room — hide the label rather than force text that would overflow;
@@ -308,7 +346,7 @@
308
346
  x={ci * colWidth + nodeWidth / 2}
309
347
  y={-8}
310
348
  text-anchor="middle"
311
- dominant-baseline="auto">{label}</text
349
+ dominant-baseline="auto">{truncateColumnLabel(label)}<title>{label}</title></text
312
350
  >
313
351
  {/each}
314
352
  {/if}
@@ -100,7 +100,16 @@
100
100
  position={tooltip.position ?? 'top'}
101
101
  testId={tooltip.testId}
102
102
  >
103
- <div class="statcard-title statcard-title-tooltip">{title}</div>
103
+ <span class="statcard-title-tooltip-trigger">
104
+ <span class="statcard-title statcard-title-tooltip">{title}</span>
105
+ <span class="statcard-tooltip-icon" aria-hidden="true">
106
+ <svg viewBox="0 0 16 16" fill="none">
107
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.2" />
108
+ <rect x="7.25" y="6.9" width="1.5" height="4.1" rx="0.75" fill="currentColor" />
109
+ <circle cx="8" cy="4.8" r="0.9" fill="currentColor" />
110
+ </svg>
111
+ </span>
112
+ </span>
104
113
  </Tooltip>
105
114
  {:else}
106
115
  <div class="statcard-title">{title}</div>
@@ -262,10 +271,15 @@
262
271
  flex-shrink: 0;
263
272
  }
264
273
 
265
- /* Transparent wrapper: keeps the checkbox in the event path (to stop bubbling to
266
- the card action) without altering the header layout. */
274
+ /* Wrapper keeps the checkbox in the event path (to stop bubbling to the card
275
+ action). `margin-left: auto` pushes it to the header's right edge so the title
276
+ stays left and the checkbox reads as a right-aligned control. (A previous
277
+ `display: contents` here removed the box, which silently defeated the flex
278
+ alignment.) */
267
279
  .statcard-header-checkbox {
268
- display: contents;
280
+ display: flex;
281
+ align-items: center;
282
+ margin-left: auto;
269
283
  }
270
284
 
271
285
  .statcard-title {
@@ -278,12 +292,38 @@
278
292
  text-overflow: ellipsis;
279
293
  }
280
294
 
295
+ .statcard-title-tooltip-trigger {
296
+ display: inline-flex;
297
+ align-items: center;
298
+ gap: var(--statcard-tooltip-icon-gap, 4px);
299
+ min-width: 0;
300
+ }
301
+
281
302
  .statcard-title-tooltip {
282
303
  cursor: default;
283
- text-decoration: underline dotted;
304
+ /* Default surface (Euler) uses the ⓘ icon as the tooltip affordance, so the
305
+ title carries no underline. The Shopify theme flips both tokens — dotted
306
+ underline on, icon off — via its own stylesheet. */
307
+ text-decoration: var(--statcard-title-tooltip-decoration, none);
284
308
  text-underline-offset: 2px;
285
309
  }
286
310
 
311
+ .statcard-tooltip-icon {
312
+ display: var(--statcard-tooltip-icon-display, inline-flex);
313
+ align-items: center;
314
+ justify-content: center;
315
+ width: var(--statcard-tooltip-icon-size, 14px);
316
+ height: var(--statcard-tooltip-icon-size, 14px);
317
+ flex-shrink: 0;
318
+ color: var(--statcard-tooltip-icon-color, currentColor);
319
+ cursor: default;
320
+ }
321
+
322
+ .statcard-tooltip-icon svg {
323
+ width: 100%;
324
+ height: 100%;
325
+ }
326
+
287
327
  .statcard-value-row {
288
328
  display: flex;
289
329
  align-items: var(--statcard-value-row-align, baseline);
@@ -121,6 +121,15 @@
121
121
  if (typeof valueA === 'number' && typeof valueB === 'number') {
122
122
  return direction === 'asc' ? valueA - valueB : valueB - valueA;
123
123
  } else if (typeof valueA === 'string' && typeof valueB === 'string') {
124
+ // Separator-formatted numeric strings (e.g. "1,11,600") must sort by their
125
+ // numeric value, not lexicographically; anything unparseable falls back to
126
+ // locale comparison. Empty strings stay in the locale branch so they are not
127
+ // coerced to 0.
128
+ const numericA = valueA.trim() === '' ? NaN : Number(valueA.replace(/,/g, ''));
129
+ const numericB = valueB.trim() === '' ? NaN : Number(valueB.replace(/,/g, ''));
130
+ if (Number.isFinite(numericA) && Number.isFinite(numericB)) {
131
+ return direction === 'asc' ? numericA - numericB : numericB - numericA;
132
+ }
124
133
  return direction === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
125
134
  } else if (typeof valueA === 'boolean' && typeof valueB === 'boolean') {
126
135
  return direction === 'asc'
@@ -120,7 +120,7 @@
120
120
  >
121
121
  {#if typeof leftIcon === 'string' && leftIcon.length > 0}
122
122
  <div class="toast-icon-wrapper">
123
- <Img src={leftIcon} alt="" />
123
+ <Img inlineSvg src={leftIcon} alt="" fallback="" />
124
124
  </div>
125
125
  {/if}
126
126
 
@@ -149,7 +149,7 @@
149
149
  }}
150
150
  data-pw={closeIconTestId}
151
151
  >
152
- <Img src={rightIcon} alt="Close" />
152
+ <Img inlineSvg src={rightIcon} alt="Close" fallback="" />
153
153
  </div>
154
154
  {/if}
155
155
  </div>
@@ -10,6 +10,14 @@ import type { BarChartDataPoint } from '../BarChart/properties';
10
10
  * *changed* `--radius` should pass the corresponding radius prop explicitly.
11
11
  */
12
12
  export declare const DEFAULT_CHART_CORNER_RADIUS = 4;
13
+ /**
14
+ * Default upper bound (px) on a chart's rendered height. Chart height is derived
15
+ * from width via `aspectRatio`, so on a wide surface (e.g. a full-width dashboard
16
+ * panel) a 16/9 chart can balloon past 700px. This default keeps charts from ever
17
+ * rendering absurdly tall; a call site that genuinely needs a taller chart passes
18
+ * an explicit `maxHeight` to override it.
19
+ */
20
+ export declare const DEFAULT_CHART_MAX_HEIGHT = 420;
13
21
  export type Margin = {
14
22
  top: number;
15
23
  right: number;
@@ -9,3 +9,11 @@
9
9
  * *changed* `--radius` should pass the corresponding radius prop explicitly.
10
10
  */
11
11
  export const DEFAULT_CHART_CORNER_RADIUS = 4;
12
+ /**
13
+ * Default upper bound (px) on a chart's rendered height. Chart height is derived
14
+ * from width via `aspectRatio`, so on a wide surface (e.g. a full-width dashboard
15
+ * panel) a 16/9 chart can balloon past 700px. This default keeps charts from ever
16
+ * rendering absurdly tall; a call site that genuinely needs a taller chart passes
17
+ * an explicit `maxHeight` to override it.
18
+ */
19
+ export const DEFAULT_CHART_MAX_HEIGHT = 420;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.80.5",
3
+ "version": "2.80.7",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",