@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.
- package/dist/BarChart/BarChart.svelte +23 -4
- package/dist/BarChart/properties.d.ts +8 -0
- package/dist/Button/Button.svelte +5 -0
- package/dist/DeltaIndicator/DeltaIndicator.svelte +8 -0
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +10 -2
- package/dist/DualAxisBarChart/properties.d.ts +7 -0
- package/dist/FunnelChart/FunnelChart.svelte +10 -2
- package/dist/FunnelChart/properties.d.ts +7 -0
- package/dist/Icon/Icon.svelte +8 -2
- package/dist/LineChart/LineChart.svelte +2 -1
- package/dist/Menu/Menu.svelte +32 -7
- package/dist/Menu/properties.d.ts +5 -0
- package/dist/Modal/Modal.svelte +24 -6
- package/dist/PieChart/PieChart.svelte +5 -0
- package/dist/PieChart/properties.d.ts +7 -0
- package/dist/SankeyChart/SankeyChart.svelte +47 -9
- package/dist/StatCard/StatCard.svelte +45 -5
- package/dist/Table/Table.svelte +9 -0
- package/dist/Toast/Toast.svelte +2 -2
- package/dist/_chart/types.d.ts +8 -0
- package/dist/_chart/types.js +8 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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. */
|
package/dist/Icon/Icon.svelte
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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 =
|
|
49
|
+
maxHeight = DEFAULT_CHART_MAX_HEIGHT,
|
|
49
50
|
tooltipSnippet,
|
|
50
51
|
empty,
|
|
51
52
|
highlightedIndex = null,
|
package/dist/Menu/Menu.svelte
CHANGED
|
@@ -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 =
|
|
47
|
+
function openMenu(startIndex: number | null = null) {
|
|
47
48
|
open = true;
|
|
48
|
-
|
|
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(
|
|
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'
|
|
240
|
-
?
|
|
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
|
-
|
|
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;
|
package/dist/Modal/Modal.svelte
CHANGED
|
@@ -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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
413
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
<
|
|
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
|
-
/*
|
|
266
|
-
|
|
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:
|
|
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
|
-
|
|
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);
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -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'
|
package/dist/Toast/Toast.svelte
CHANGED
|
@@ -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>
|
package/dist/_chart/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/_chart/types.js
CHANGED
|
@@ -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;
|