@juspay/svelte-ui-components 2.60.0 → 2.62.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.
- package/dist/PieChart/PieChart.svelte +139 -17
- package/dist/PieChart/properties.d.ts +3 -0
- package/dist/Stepper/Step.svelte +165 -16
- package/dist/Stepper/Stepper.svelte +115 -15
- package/dist/Stepper/properties.d.ts +45 -9
- package/dist/_chart/ChartContainer.svelte +24 -9
- package/dist/_chart/format.d.ts +1 -0
- package/dist/_chart/format.js +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { arcPath } from '../_chart/paths';
|
|
7
7
|
import { computePieLayout } from '../_chart/geometry';
|
|
8
8
|
import { getColor } from '../_chart/colors';
|
|
9
|
-
import { formatNumber
|
|
9
|
+
import { formatNumber } from '../_chart/format';
|
|
10
10
|
import type { LegendItem } from '../_chart/types';
|
|
11
11
|
|
|
12
12
|
// ── Props ──────────────────────────────────────────────────────
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
labelPosition = 'outside',
|
|
21
21
|
showLegend = false,
|
|
22
22
|
startAngle = -Math.PI / 2,
|
|
23
|
-
aspectRatio
|
|
23
|
+
aspectRatio,
|
|
24
24
|
valueFormat,
|
|
25
25
|
tooltipSnippet,
|
|
26
26
|
center,
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
onsliceclick,
|
|
29
29
|
onslicehover,
|
|
30
30
|
testId,
|
|
31
|
-
classes
|
|
31
|
+
classes,
|
|
32
|
+
semiCircle = false,
|
|
33
|
+
legendShowValues = false,
|
|
34
|
+
percentDecimals = 0
|
|
32
35
|
}: PieChartProperties = $props();
|
|
33
36
|
|
|
34
37
|
// ── State ──────────────────────────────────────────────────────
|
|
@@ -40,32 +43,93 @@
|
|
|
40
43
|
let mouseX = $state(0);
|
|
41
44
|
let mouseY = $state(0);
|
|
42
45
|
|
|
46
|
+
// Aspect ratio read from --piechart-semi-aspect-ratio CSS variable.
|
|
47
|
+
// Uses $effect so it re-reads whenever containerEl binds or semiCircle changes
|
|
48
|
+
// (e.g. media-query theme switch), not just on initial mount.
|
|
49
|
+
let semiAspectRatioCssVar = $state(2);
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
52
|
+
$effect(() => {
|
|
53
|
+
// Track semiCircle and containerEl so the effect re-runs when either changes.
|
|
54
|
+
void semiCircle;
|
|
55
|
+
void containerEl;
|
|
56
|
+
if (typeof window === 'undefined' || containerEl === null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const rawValue = getComputedStyle(containerEl)
|
|
60
|
+
.getPropertyValue('--piechart-semi-aspect-ratio')
|
|
61
|
+
.trim();
|
|
62
|
+
const parsed = parseFloat(rawValue);
|
|
63
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
64
|
+
semiAspectRatioCssVar = parsed;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
43
68
|
// ── Layout ─────────────────────────────────────────────────────
|
|
44
69
|
|
|
45
70
|
let format = $derived(valueFormat ?? formatNumber);
|
|
46
71
|
let total = $derived(data.reduce((sum, d) => sum + Math.max(0, d.value), 0));
|
|
72
|
+
let pctFormat = $derived.by(
|
|
73
|
+
() =>
|
|
74
|
+
(v: number): string =>
|
|
75
|
+
total === 0 ? '0%' : ((v / total) * 100).toFixed(percentDecimals) + '%'
|
|
76
|
+
);
|
|
47
77
|
let isEmpty = $derived(data.length === 0 || total === 0);
|
|
48
78
|
|
|
79
|
+
// When semiCircle is true the effective aspect ratio is driven by:
|
|
80
|
+
// 1. The explicit `aspectRatio` prop (highest priority — always wins).
|
|
81
|
+
// 2. The `--piechart-semi-aspect-ratio` CSS variable (consumer CSS override).
|
|
82
|
+
// 3. The hardcoded default of 2 (width:height = 2:1).
|
|
83
|
+
// For a full circle the caller-provided `aspectRatio` or a square (1:1) default is used.
|
|
84
|
+
let effectiveAspectRatio = $derived(aspectRatio ?? (semiCircle ? semiAspectRatioCssVar : 1));
|
|
85
|
+
|
|
49
86
|
let cx = $derived(chartWidth / 2);
|
|
50
|
-
|
|
87
|
+
// For a half-donut the SVG origin sits at the bottom of the drawing area so
|
|
88
|
+
// arcs radiate upward into the top half of the viewBox.
|
|
89
|
+
let cy = $derived(semiCircle ? chartHeight : chartHeight / 2);
|
|
51
90
|
let outerR = $derived(
|
|
52
|
-
|
|
91
|
+
semiCircle
|
|
92
|
+
? Math.max(10, chartWidth / 2 - (showLabels && labelPosition === 'outside' ? 40 : 10))
|
|
93
|
+
: Math.max(10, Math.min(cx, cy) - (showLabels && labelPosition === 'outside' ? 40 : 10))
|
|
53
94
|
);
|
|
54
95
|
let innerR = $derived(innerRadius > 0 ? outerR * Math.min(0.95, innerRadius) : 0);
|
|
55
96
|
|
|
56
|
-
let slices = $derived(
|
|
57
|
-
|
|
97
|
+
let slices = $derived.by(() => {
|
|
98
|
+
// For a full circle layout start at the caller-provided startAngle.
|
|
99
|
+
// For semi-circle: compute a full-circle layout anchored at -PI/2, then
|
|
100
|
+
// remap each angle so the entire sweep is compressed into PI radians
|
|
101
|
+
// (the top-half arc from -PI/2 to PI/2).
|
|
102
|
+
const layoutStartAngle = semiCircle ? -Math.PI / 2 : startAngle;
|
|
103
|
+
const rawSlices = computePieLayout(data, layoutStartAngle, padAngle);
|
|
104
|
+
|
|
105
|
+
return rawSlices.map((s) => {
|
|
106
|
+
let mappedStart = s.startAngle;
|
|
107
|
+
let mappedEnd = s.endAngle;
|
|
108
|
+
let mappedMid = s.midAngle;
|
|
109
|
+
|
|
110
|
+
if (semiCircle) {
|
|
111
|
+
// The raw layout spans [-PI/2, -PI/2 + 2*PI]. We compress it to
|
|
112
|
+
// [-PI/2, PI/2] by halving the angular distance from -PI/2.
|
|
113
|
+
const origin = -Math.PI / 2;
|
|
114
|
+
mappedStart = origin + (s.startAngle - origin) / 2;
|
|
115
|
+
mappedEnd = origin + (s.endAngle - origin) / 2;
|
|
116
|
+
mappedMid = origin + (s.midAngle - origin) / 2;
|
|
117
|
+
}
|
|
118
|
+
|
|
58
119
|
const color = s.color ?? data[s.index]?.color ?? getColor(s.index);
|
|
59
120
|
const labelR = labelPosition === 'outside' ? outerR + 16 : (innerR + outerR) / 2;
|
|
60
121
|
return {
|
|
61
122
|
...s,
|
|
123
|
+
startAngle: mappedStart,
|
|
124
|
+
endAngle: mappedEnd,
|
|
125
|
+
midAngle: mappedMid,
|
|
62
126
|
color,
|
|
63
|
-
path: arcPath(0, 0, innerR, outerR,
|
|
64
|
-
labelX: labelR * Math.cos(
|
|
65
|
-
labelY: labelR * Math.sin(
|
|
127
|
+
path: arcPath(0, 0, innerR, outerR, mappedStart, mappedEnd),
|
|
128
|
+
labelX: labelR * Math.cos(mappedMid),
|
|
129
|
+
labelY: labelR * Math.sin(mappedMid)
|
|
66
130
|
};
|
|
67
|
-
})
|
|
68
|
-
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
69
133
|
|
|
70
134
|
let legendItems = $derived<LegendItem[]>(
|
|
71
135
|
data.map((d, i) => ({ label: d.label, color: d.color ?? getColor(i) }))
|
|
@@ -73,6 +137,13 @@
|
|
|
73
137
|
|
|
74
138
|
let centerBoxSize = $derived(innerR > 0 ? Math.max(0, innerR * 1.3) : 0);
|
|
75
139
|
|
|
140
|
+
// The foreignObject for the center snippet is positioned relative to the <g>
|
|
141
|
+
// origin (which is at cx, cy in SVG space). The box is always centred on
|
|
142
|
+
// the translated origin: for a full circle that is the geometric centre, and
|
|
143
|
+
// for a semiCircle the <g> origin sits at the chord line (bottom of the arc),
|
|
144
|
+
// so the snippet is centred on the chord as specified.
|
|
145
|
+
let centerFOY = $derived(-centerBoxSize / 2);
|
|
146
|
+
|
|
76
147
|
// ── Tooltip ────────────────────────────────────────────────────
|
|
77
148
|
|
|
78
149
|
let tooltipData = $derived.by(() => {
|
|
@@ -85,7 +156,7 @@
|
|
|
85
156
|
items: [
|
|
86
157
|
{
|
|
87
158
|
label: s.label,
|
|
88
|
-
value: `${format(s.value)} (${
|
|
159
|
+
value: `${format(s.value)} (${pctFormat(s.value)})`,
|
|
89
160
|
color: s.color
|
|
90
161
|
}
|
|
91
162
|
]
|
|
@@ -123,11 +194,15 @@
|
|
|
123
194
|
{#if isEmpty && typeof empty === 'function'}
|
|
124
195
|
<div class="chart-empty">{@render empty()}</div>
|
|
125
196
|
{:else}
|
|
126
|
-
{#if showLegend}
|
|
197
|
+
{#if showLegend && !legendShowValues}
|
|
127
198
|
<Legend items={legendItems} position="top" />
|
|
128
199
|
{/if}
|
|
129
200
|
|
|
130
|
-
<ChartContainer
|
|
201
|
+
<ChartContainer
|
|
202
|
+
bind:width={chartWidth}
|
|
203
|
+
bind:height={chartHeight}
|
|
204
|
+
aspectRatio={effectiveAspectRatio}
|
|
205
|
+
>
|
|
131
206
|
<g transform="translate({cx}, {cy})">
|
|
132
207
|
{#each slices as slice (slice.index)}
|
|
133
208
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -155,7 +230,7 @@
|
|
|
155
230
|
>
|
|
156
231
|
{#if showLabels}{slice.label}{/if}
|
|
157
232
|
{#if showValues}
|
|
158
|
-
{
|
|
233
|
+
{pctFormat(slice.value)}{/if}
|
|
159
234
|
</text>
|
|
160
235
|
{/if}
|
|
161
236
|
{/each}
|
|
@@ -163,7 +238,7 @@
|
|
|
163
238
|
{#if innerR > 0 && typeof center === 'function' && centerBoxSize > 0}
|
|
164
239
|
<foreignObject
|
|
165
240
|
x={-centerBoxSize / 2}
|
|
166
|
-
y={
|
|
241
|
+
y={centerFOY}
|
|
167
242
|
width={centerBoxSize}
|
|
168
243
|
height={centerBoxSize}
|
|
169
244
|
>
|
|
@@ -175,6 +250,20 @@
|
|
|
175
250
|
</g>
|
|
176
251
|
</ChartContainer>
|
|
177
252
|
|
|
253
|
+
{#if showLegend && legendShowValues}
|
|
254
|
+
<ul class="pie-legend-values">
|
|
255
|
+
{#each data as d, i (i)}
|
|
256
|
+
<li class="pie-legend-row">
|
|
257
|
+
<span class="pie-legend-swatch" style="background: {d.color ?? getColor(i)}"></span>
|
|
258
|
+
<span class="pie-legend-label">{d.label}</span>
|
|
259
|
+
<span class="pie-legend-value">
|
|
260
|
+
{format(d.value)} {pctFormat(d.value)}
|
|
261
|
+
</span>
|
|
262
|
+
</li>
|
|
263
|
+
{/each}
|
|
264
|
+
</ul>
|
|
265
|
+
{/if}
|
|
266
|
+
|
|
178
267
|
{#if typeof tooltipSnippet === 'function' && hoveredIndex !== null && data[hoveredIndex]}
|
|
179
268
|
<div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
|
|
180
269
|
{@render tooltipSnippet(data[hoveredIndex], hoveredIndex)}
|
|
@@ -233,4 +322,37 @@
|
|
|
233
322
|
color: var(--chart-empty-color, #9ca3af);
|
|
234
323
|
text-align: center;
|
|
235
324
|
}
|
|
325
|
+
.pie-legend-values {
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
gap: var(--piechart-legend-gap, 8px);
|
|
329
|
+
padding: var(--piechart-legend-padding, 12px 0 0 0);
|
|
330
|
+
font-family: var(--chart-font-family, inherit);
|
|
331
|
+
list-style: none;
|
|
332
|
+
margin: 0;
|
|
333
|
+
}
|
|
334
|
+
.pie-legend-row {
|
|
335
|
+
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: var(--piechart-legend-row-gap, 6px);
|
|
338
|
+
}
|
|
339
|
+
.pie-legend-swatch {
|
|
340
|
+
display: inline-block;
|
|
341
|
+
width: var(--chart-legend-swatch-size, 12px);
|
|
342
|
+
height: var(--chart-legend-swatch-size, 12px);
|
|
343
|
+
border-radius: var(--piechart-legend-swatch-radius, 2px);
|
|
344
|
+
flex-shrink: 0;
|
|
345
|
+
}
|
|
346
|
+
.pie-legend-label {
|
|
347
|
+
font-size: var(--chart-legend-font-size, 12px);
|
|
348
|
+
color: var(--chart-legend-color, #333);
|
|
349
|
+
min-width: var(--piechart-legend-label-min-width, 120px);
|
|
350
|
+
}
|
|
351
|
+
.pie-legend-value {
|
|
352
|
+
margin-left: auto;
|
|
353
|
+
font-size: var(--piechart-legend-value-font-size, 12px);
|
|
354
|
+
color: var(--piechart-legend-value-color, #333);
|
|
355
|
+
min-width: var(--piechart-legend-value-min-width, 60px);
|
|
356
|
+
text-align: right;
|
|
357
|
+
}
|
|
236
358
|
</style>
|
|
@@ -23,6 +23,9 @@ export type OptionalPieChartProperties = {
|
|
|
23
23
|
empty?: Snippet;
|
|
24
24
|
testId?: string;
|
|
25
25
|
classes?: string;
|
|
26
|
+
semiCircle?: boolean;
|
|
27
|
+
legendShowValues?: boolean;
|
|
28
|
+
percentDecimals?: number;
|
|
26
29
|
};
|
|
27
30
|
export type PieChartEventProperties = {
|
|
28
31
|
onsliceclick?: (event: {
|
package/dist/Stepper/Step.svelte
CHANGED
|
@@ -1,18 +1,77 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { StepProperties } from './properties';
|
|
3
3
|
|
|
4
|
-
let {
|
|
4
|
+
let {
|
|
5
|
+
stepIndex,
|
|
6
|
+
label,
|
|
7
|
+
icon,
|
|
8
|
+
status,
|
|
9
|
+
badge,
|
|
10
|
+
orientation = 'horizontal',
|
|
11
|
+
classes,
|
|
12
|
+
ariaLabel,
|
|
13
|
+
onclick,
|
|
14
|
+
onkeydown
|
|
15
|
+
}: StepProperties = $props();
|
|
5
16
|
|
|
6
|
-
|
|
17
|
+
const handleStepClick = (): void => {
|
|
7
18
|
onclick?.({ selectedIndex: stepIndex });
|
|
8
|
-
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleKeydown = (event: KeyboardEvent): void => {
|
|
22
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
handleStepClick();
|
|
25
|
+
}
|
|
26
|
+
onkeydown?.(event);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let isVertical = $derived(orientation === 'vertical');
|
|
30
|
+
|
|
31
|
+
let stepClass = $derived(
|
|
32
|
+
['step', isVertical ? 'step-vertical' : '', classes ?? ''].filter((c) => c.length > 0).join(' ')
|
|
33
|
+
);
|
|
9
34
|
</script>
|
|
10
35
|
|
|
11
|
-
<div
|
|
36
|
+
<div
|
|
37
|
+
class={stepClass}
|
|
38
|
+
onclick={handleStepClick}
|
|
39
|
+
onkeydown={handleKeydown}
|
|
40
|
+
role="button"
|
|
41
|
+
tabindex="0"
|
|
42
|
+
aria-label={ariaLabel ?? null}
|
|
43
|
+
aria-labelledby={ariaLabel ? null : `step-label-${stepIndex}`}
|
|
44
|
+
aria-current={status === 'active' ? 'step' : null}
|
|
45
|
+
>
|
|
12
46
|
{#if typeof icon === 'string' && icon.length > 0}
|
|
13
47
|
<div class="step-icon-container">
|
|
14
48
|
<img class="step-icon" src={icon} alt="" />
|
|
15
49
|
</div>
|
|
50
|
+
{:else if status === 'in-progress'}
|
|
51
|
+
<div class="step-index-container">
|
|
52
|
+
<svg
|
|
53
|
+
class="step-spinner"
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
fill="none"
|
|
56
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
>
|
|
59
|
+
<circle
|
|
60
|
+
cx="12"
|
|
61
|
+
cy="12"
|
|
62
|
+
r="9"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
stroke-opacity="0.25"
|
|
65
|
+
stroke-width="3"
|
|
66
|
+
/>
|
|
67
|
+
<path
|
|
68
|
+
d="M12 3a9 9 0 0 1 9 9"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
stroke-width="3"
|
|
71
|
+
stroke-linecap="round"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
</div>
|
|
16
75
|
{:else}
|
|
17
76
|
<div class="step-index-container">
|
|
18
77
|
<div class="step-index-text">
|
|
@@ -20,9 +79,17 @@
|
|
|
20
79
|
</div>
|
|
21
80
|
</div>
|
|
22
81
|
{/if}
|
|
23
|
-
|
|
82
|
+
|
|
83
|
+
<div class="step-text" id="step-label-{stepIndex}">
|
|
24
84
|
{label}
|
|
25
85
|
</div>
|
|
86
|
+
|
|
87
|
+
{#if typeof badge === 'function'}
|
|
88
|
+
<div class="step-badge">
|
|
89
|
+
{@render badge()}
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
26
93
|
<div class="separator"></div>
|
|
27
94
|
</div>
|
|
28
95
|
|
|
@@ -33,6 +100,11 @@
|
|
|
33
100
|
align-items: center;
|
|
34
101
|
}
|
|
35
102
|
|
|
103
|
+
.step-vertical {
|
|
104
|
+
--step-flex-direction: column;
|
|
105
|
+
align-items: flex-start;
|
|
106
|
+
}
|
|
107
|
+
|
|
36
108
|
.step-index-container {
|
|
37
109
|
display: flex;
|
|
38
110
|
justify-content: center;
|
|
@@ -41,33 +113,110 @@
|
|
|
41
113
|
width: var(--step-index-container-width, 30px);
|
|
42
114
|
border-radius: var(--step-index-container-radius, 50%);
|
|
43
115
|
background-color: var(--step-index-container-background-color, #798fa5cc);
|
|
116
|
+
color: var(--step-index-color, white);
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.step-icon-container {
|
|
121
|
+
display: flex;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
align-items: center;
|
|
124
|
+
height: var(--step-index-container-height, 30px);
|
|
125
|
+
width: var(--step-index-container-width, 30px);
|
|
126
|
+
border-radius: var(--step-index-container-radius, 50%);
|
|
127
|
+
background-color: var(--step-index-container-background-color, #798fa5cc);
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.step-icon {
|
|
133
|
+
width: var(--step-icon-size, 18px);
|
|
134
|
+
height: var(--step-icon-size, 18px);
|
|
135
|
+
object-fit: contain;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.step-spinner {
|
|
139
|
+
width: var(--step-spinner-size, 18px);
|
|
140
|
+
height: var(--step-spinner-size, 18px);
|
|
141
|
+
animation: stepper-spin 0.8s linear infinite;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@keyframes stepper-spin {
|
|
145
|
+
from {
|
|
146
|
+
transform: rotate(0deg);
|
|
147
|
+
}
|
|
148
|
+
to {
|
|
149
|
+
transform: rotate(360deg);
|
|
150
|
+
}
|
|
44
151
|
}
|
|
45
152
|
|
|
46
153
|
.separator {
|
|
47
|
-
display: var(--separator-display, block);
|
|
48
|
-
height: var(--separator-height, 1px);
|
|
49
|
-
width: var(--separator-width, 50px);
|
|
50
|
-
margin: var(--separator-margin, 0px 12px 0px 12px);
|
|
154
|
+
display: var(--stepper-separator-display, var(--separator-display, block));
|
|
155
|
+
height: var(--stepper-separator-height, var(--separator-height, 1px));
|
|
156
|
+
width: var(--stepper-separator-width, var(--separator-width, 50px));
|
|
157
|
+
margin: var(--stepper-separator-margin, var(--separator-margin, 0px 12px 0px 12px));
|
|
51
158
|
background-image: var(
|
|
52
|
-
--separator-background-image,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
159
|
+
--stepper-separator-background-image,
|
|
160
|
+
var(
|
|
161
|
+
--separator-background-image,
|
|
162
|
+
repeating-linear-gradient(
|
|
163
|
+
to right,
|
|
164
|
+
var(
|
|
165
|
+
--stepper-separator-background-image-color,
|
|
166
|
+
var(--separator-background-image-color, #798fa5cc)
|
|
167
|
+
),
|
|
168
|
+
var(
|
|
169
|
+
--stepper-separator-background-image-color,
|
|
170
|
+
var(--separator-background-image-color, #798fa5cc)
|
|
171
|
+
)
|
|
172
|
+
6px,
|
|
173
|
+
transparent 6px,
|
|
174
|
+
transparent 10px
|
|
175
|
+
)
|
|
59
176
|
)
|
|
60
177
|
);
|
|
61
178
|
}
|
|
62
179
|
|
|
180
|
+
.step-vertical .separator {
|
|
181
|
+
height: var(--stepper-separator-vertical-height, 32px);
|
|
182
|
+
width: var(--stepper-separator-vertical-width, 1px);
|
|
183
|
+
margin: var(--stepper-separator-vertical-margin, 4px 0px 4px 14px);
|
|
184
|
+
background-image: repeating-linear-gradient(
|
|
185
|
+
to bottom,
|
|
186
|
+
var(
|
|
187
|
+
--stepper-separator-background-image-color,
|
|
188
|
+
var(--separator-background-image-color, #798fa5cc)
|
|
189
|
+
),
|
|
190
|
+
var(
|
|
191
|
+
--stepper-separator-background-image-color,
|
|
192
|
+
var(--separator-background-image-color, #798fa5cc)
|
|
193
|
+
)
|
|
194
|
+
6px,
|
|
195
|
+
transparent 6px,
|
|
196
|
+
transparent 10px
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
63
200
|
.step-text {
|
|
64
201
|
margin: var(--step-text-margin, 0px 0px 0px 12px);
|
|
65
202
|
font-size: var(--step-text-font-size, 12px);
|
|
66
203
|
color: var(--step-text-color, #798fa5cc);
|
|
67
204
|
}
|
|
68
205
|
|
|
206
|
+
.step-vertical .step-text {
|
|
207
|
+
margin: var(--step-text-vertical-margin, 4px 0px 0px 0px);
|
|
208
|
+
}
|
|
209
|
+
|
|
69
210
|
.step-index-text {
|
|
70
211
|
font-size: var(--step-index-font-size, 14px);
|
|
71
212
|
color: var(--step-index-color, white);
|
|
72
213
|
}
|
|
214
|
+
|
|
215
|
+
.step-badge {
|
|
216
|
+
margin: var(--step-badge-margin, 0 0 0 4px);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.step-vertical .step-badge {
|
|
220
|
+
margin: var(--step-badge-vertical-margin, 4px 0 0 0);
|
|
221
|
+
}
|
|
73
222
|
</style>
|
|
@@ -1,22 +1,57 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { StepperProperties } from './properties';
|
|
2
|
+
import type { StepperProperties, StepStatus } from './properties';
|
|
3
3
|
import Step from './Step.svelte';
|
|
4
4
|
|
|
5
|
-
let {
|
|
5
|
+
let {
|
|
6
|
+
steps,
|
|
7
|
+
currentStepIndex,
|
|
8
|
+
orientation = 'horizontal',
|
|
9
|
+
classes,
|
|
10
|
+
testId,
|
|
11
|
+
onstepclick,
|
|
12
|
+
onhandleStepClick
|
|
13
|
+
}: StepperProperties = $props();
|
|
14
|
+
|
|
15
|
+
const resolveStatus = (stepIndex: number, explicitStatus: StepStatus | null): StepStatus => {
|
|
16
|
+
if (explicitStatus !== null) {
|
|
17
|
+
return explicitStatus;
|
|
18
|
+
}
|
|
19
|
+
if (stepIndex < currentStepIndex) {
|
|
20
|
+
return 'completed';
|
|
21
|
+
}
|
|
22
|
+
if (stepIndex === currentStepIndex) {
|
|
23
|
+
return 'active';
|
|
24
|
+
}
|
|
25
|
+
return 'pending';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Support the deprecated onhandleStepClick alias — onstepclick takes priority.
|
|
29
|
+
const effectiveStepClick = $derived(onstepclick ?? onhandleStepClick);
|
|
30
|
+
|
|
31
|
+
let containerClass = $derived(
|
|
32
|
+
['container', orientation === 'vertical' ? 'container-vertical' : '', classes ?? '']
|
|
33
|
+
.filter((c) => c.length > 0)
|
|
34
|
+
.join(' ')
|
|
35
|
+
);
|
|
6
36
|
</script>
|
|
7
37
|
|
|
8
|
-
<div class=
|
|
38
|
+
<div class={containerClass} data-pw={typeof testId === 'string' ? testId : null} role="list">
|
|
9
39
|
{#each steps as currentStep, stepIndex (stepIndex)}
|
|
40
|
+
{@const effectiveStatus = resolveStatus(stepIndex, currentStep.status ?? null)}
|
|
10
41
|
<div
|
|
11
|
-
|
|
12
|
-
class
|
|
13
|
-
|
|
42
|
+
role="listitem"
|
|
43
|
+
class="step-container status-{effectiveStatus} {effectiveStatus === 'active'
|
|
44
|
+
? 'active-step'
|
|
45
|
+
: ''} {effectiveStatus === 'completed' ? 'completed-step' : ''}"
|
|
14
46
|
>
|
|
15
47
|
<Step
|
|
16
|
-
onclick={
|
|
48
|
+
onclick={effectiveStepClick}
|
|
17
49
|
label={currentStep.label}
|
|
18
50
|
icon={currentStep.icon}
|
|
19
51
|
stepIndex={stepIndex + 1}
|
|
52
|
+
status={effectiveStatus}
|
|
53
|
+
badge={currentStep.badge}
|
|
54
|
+
{orientation}
|
|
20
55
|
/>
|
|
21
56
|
</div>
|
|
22
57
|
{/each}
|
|
@@ -29,8 +64,13 @@
|
|
|
29
64
|
align-items: center;
|
|
30
65
|
}
|
|
31
66
|
|
|
67
|
+
.container-vertical {
|
|
68
|
+
--container-flex-direction: column;
|
|
69
|
+
align-items: flex-start;
|
|
70
|
+
}
|
|
71
|
+
|
|
32
72
|
.step-container:last-child {
|
|
33
|
-
--separator-display: none;
|
|
73
|
+
--stepper-separator-display: none;
|
|
34
74
|
}
|
|
35
75
|
|
|
36
76
|
.step-container {
|
|
@@ -38,21 +78,81 @@
|
|
|
38
78
|
align-items: center;
|
|
39
79
|
}
|
|
40
80
|
|
|
41
|
-
|
|
81
|
+
/* status-completed */
|
|
82
|
+
.status-completed {
|
|
83
|
+
--step-text-color: var(--step-text-completed-color, #24aa5a);
|
|
84
|
+
--stepper-separator-background-image-color: var(
|
|
85
|
+
--stepper-separator-background-image-completed-color,
|
|
86
|
+
#24aa5a
|
|
87
|
+
);
|
|
88
|
+
--step-index-container-background-color: var(
|
|
89
|
+
--step-index-container-completed-background-color,
|
|
90
|
+
#24aa5a
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* status-active */
|
|
95
|
+
.status-active {
|
|
42
96
|
--step-text-color: var(--step-text-active-color, #2f3841);
|
|
43
|
-
--separator-background-image-color: var(
|
|
97
|
+
--stepper-separator-background-image-color: var(
|
|
98
|
+
--stepper-separator-background-image-active-color,
|
|
99
|
+
#2f3841
|
|
100
|
+
);
|
|
44
101
|
--step-index-container-background-color: var(
|
|
45
102
|
--step-index-container-active-background-color,
|
|
46
103
|
#2f3841
|
|
47
104
|
);
|
|
48
105
|
}
|
|
49
106
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
107
|
+
/* status-pending: no override — falls through to #798fa5cc grey default in Step.svelte */
|
|
108
|
+
|
|
109
|
+
/* status-failure */
|
|
110
|
+
.status-failure {
|
|
111
|
+
--step-text-color: var(--step-text-failure-color, #e53935);
|
|
112
|
+
--stepper-separator-background-image-color: var(
|
|
113
|
+
--stepper-separator-background-image-failure-color,
|
|
114
|
+
#e53935
|
|
115
|
+
);
|
|
53
116
|
--step-index-container-background-color: var(
|
|
54
|
-
--step-index-container-
|
|
55
|
-
#
|
|
117
|
+
--step-index-container-failure-background-color,
|
|
118
|
+
var(--stepper-status-failure-color, #e53935)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* status-in-progress */
|
|
123
|
+
.status-in-progress {
|
|
124
|
+
--step-text-color: var(--step-text-in-progress-color, #f59e0b);
|
|
125
|
+
--stepper-separator-background-image-color: var(
|
|
126
|
+
--stepper-separator-background-image-in-progress-color,
|
|
127
|
+
#f59e0b
|
|
128
|
+
);
|
|
129
|
+
--step-index-container-background-color: var(
|
|
130
|
+
--step-index-container-in-progress-background-color,
|
|
131
|
+
var(--stepper-status-in-progress-color, #f59e0b)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
:global([theme='dark']) .status-failure {
|
|
136
|
+
--step-text-color: var(--step-text-failure-color, #ef5350);
|
|
137
|
+
--stepper-separator-background-image-color: var(
|
|
138
|
+
--stepper-separator-background-image-failure-color,
|
|
139
|
+
#ef5350
|
|
140
|
+
);
|
|
141
|
+
--step-index-container-background-color: var(
|
|
142
|
+
--step-index-container-failure-background-color,
|
|
143
|
+
var(--stepper-status-failure-color, #ef5350)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
:global([theme='dark']) .status-in-progress {
|
|
148
|
+
--step-text-color: var(--step-text-in-progress-color, #fbbf24);
|
|
149
|
+
--stepper-separator-background-image-color: var(
|
|
150
|
+
--stepper-separator-background-image-in-progress-color,
|
|
151
|
+
#fbbf24
|
|
152
|
+
);
|
|
153
|
+
--step-index-container-background-color: var(
|
|
154
|
+
--step-index-container-in-progress-background-color,
|
|
155
|
+
var(--stepper-status-in-progress-color, #fbbf24)
|
|
56
156
|
);
|
|
57
157
|
}
|
|
58
158
|
</style>
|
|
@@ -1,21 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type StepStatus = 'completed' | 'active' | 'pending' | 'failure' | 'in-progress';
|
|
3
|
+
export type Step = {
|
|
4
|
+
label: string;
|
|
5
|
+
/**
|
|
6
|
+
* URL of a custom icon image. When provided, the icon takes precedence over
|
|
7
|
+
* any `status`-driven rendering (including the `in-progress` spinner).
|
|
8
|
+
*/
|
|
9
|
+
icon?: string;
|
|
10
|
+
status?: StepStatus;
|
|
11
|
+
/**
|
|
12
|
+
* Optional Svelte snippet rendered inline after the step label (to its right
|
|
13
|
+
* in horizontal layout, below it in vertical layout). Use it for badges,
|
|
14
|
+
* tags, or other inline metadata — e.g. a count pill, a status chip, or a
|
|
15
|
+
* "New" label.
|
|
16
|
+
*/
|
|
17
|
+
badge?: Snippet;
|
|
18
|
+
};
|
|
19
|
+
export type MandatoryStepperProperties = {
|
|
2
20
|
steps: Array<Step>;
|
|
3
21
|
currentStepIndex: number;
|
|
22
|
+
};
|
|
23
|
+
export type OptionalStepperProperties = {
|
|
24
|
+
orientation?: 'horizontal' | 'vertical';
|
|
4
25
|
classes?: string;
|
|
26
|
+
testId?: string;
|
|
27
|
+
};
|
|
28
|
+
export type StepperEventProperties = {
|
|
29
|
+
onstepclick?: (event: {
|
|
30
|
+
selectedIndex: number;
|
|
31
|
+
}) => void;
|
|
32
|
+
/** @deprecated Use `onstepclick` instead. */
|
|
5
33
|
onhandleStepClick?: (event: {
|
|
6
34
|
selectedIndex: number;
|
|
7
35
|
}) => void;
|
|
8
36
|
};
|
|
9
|
-
export type
|
|
10
|
-
label: string;
|
|
11
|
-
icon?: string;
|
|
12
|
-
};
|
|
13
|
-
export type StepProperties = OptionalStepProperties & StepEventProperties & {
|
|
14
|
-
stepIndex: number;
|
|
15
|
-
label: string;
|
|
16
|
-
};
|
|
37
|
+
export type StepperProperties = MandatoryStepperProperties & OptionalStepperProperties & StepperEventProperties;
|
|
17
38
|
export type OptionalStepProperties = {
|
|
18
39
|
icon?: string;
|
|
40
|
+
status?: StepStatus;
|
|
41
|
+
badge?: Snippet;
|
|
19
42
|
classes?: string;
|
|
20
43
|
};
|
|
21
44
|
export type StepEventProperties = {
|
|
@@ -24,3 +47,16 @@ export type StepEventProperties = {
|
|
|
24
47
|
}) => void;
|
|
25
48
|
onkeydown?: (event: KeyboardEvent) => void;
|
|
26
49
|
};
|
|
50
|
+
export type StepProperties = OptionalStepProperties & StepEventProperties & {
|
|
51
|
+
/** 1-based display index — Stepper passes `stepIndex + 1` so that clicking step 1 returns `selectedIndex: 1`. */
|
|
52
|
+
stepIndex: number;
|
|
53
|
+
label: string;
|
|
54
|
+
orientation?: 'horizontal' | 'vertical';
|
|
55
|
+
/**
|
|
56
|
+
* Accessible label for the step button. When provided, set as `aria-label` on
|
|
57
|
+
* the `role="button"` element, overriding the default `aria-labelledby` association
|
|
58
|
+
* with the visible label text. Use when the visible label alone is insufficient
|
|
59
|
+
* context for screen reader users (e.g. "Step 1 — Cart (completed)").
|
|
60
|
+
*/
|
|
61
|
+
ariaLabel?: string;
|
|
62
|
+
};
|
|
@@ -13,22 +13,36 @@
|
|
|
13
13
|
}: ChartContainerProperties = $props();
|
|
14
14
|
|
|
15
15
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
16
|
+
let isMounted = false;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
function measure() {
|
|
18
19
|
if (containerEl === null) {
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
22
|
+
const rect = containerEl.getBoundingClientRect();
|
|
23
|
+
const w = Math.round(rect.width);
|
|
24
|
+
width = w;
|
|
25
|
+
height = Math.max(minHeight, Math.round(w / aspectRatio));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Re-measure whenever aspectRatio changes at runtime (e.g. semiCircle toggled).
|
|
29
|
+
// isMounted guards against running after the onMount cleanup has disconnected
|
|
30
|
+
// the ResizeObserver and the component is being torn down.
|
|
31
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
32
|
+
$effect(() => {
|
|
33
|
+
// Reading aspectRatio here makes this effect re-run whenever it changes.
|
|
34
|
+
void aspectRatio;
|
|
35
|
+
if (isMounted) {
|
|
36
|
+
measure();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
21
39
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
const rect = containerEl.getBoundingClientRect();
|
|
27
|
-
const w = Math.round(rect.width);
|
|
28
|
-
width = w;
|
|
29
|
-
height = Math.max(minHeight, Math.round(w / aspectRatio));
|
|
40
|
+
onMount(() => {
|
|
41
|
+
if (containerEl === null) {
|
|
42
|
+
return;
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
isMounted = true;
|
|
32
46
|
measure();
|
|
33
47
|
|
|
34
48
|
// Coalesce bursts of resize events into a single measure per frame, always
|
|
@@ -41,6 +55,7 @@
|
|
|
41
55
|
observer.observe(containerEl);
|
|
42
56
|
|
|
43
57
|
return () => {
|
|
58
|
+
isMounted = false;
|
|
44
59
|
cancelAnimationFrame(frame);
|
|
45
60
|
observer.disconnect();
|
|
46
61
|
};
|
package/dist/_chart/format.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function formatNumber(value: number): string;
|
|
2
2
|
export declare function formatPercent(value: number, total: number): string;
|
|
3
3
|
export declare function defaultTickFormat(value: number | string): string;
|
|
4
|
+
export declare function formatNumberIndian(value: number): string;
|
package/dist/_chart/format.js
CHANGED
|
@@ -26,3 +26,16 @@ export function defaultTickFormat(value) {
|
|
|
26
26
|
}
|
|
27
27
|
return formatNumber(value);
|
|
28
28
|
}
|
|
29
|
+
export function formatNumberIndian(value) {
|
|
30
|
+
const abs = Math.abs(value);
|
|
31
|
+
if (abs >= 1e7) {
|
|
32
|
+
return (value / 1e7).toFixed(2).replace(/\.?0+$/, '') + 'Cr';
|
|
33
|
+
}
|
|
34
|
+
if (abs >= 1e5) {
|
|
35
|
+
return (value / 1e5).toFixed(2).replace(/\.?0+$/, '') + 'L';
|
|
36
|
+
}
|
|
37
|
+
if (abs >= 1e3) {
|
|
38
|
+
return (value / 1e3).toFixed(2).replace(/\.?0+$/, '') + 'K';
|
|
39
|
+
}
|
|
40
|
+
return value.toLocaleString('en-IN');
|
|
41
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -65,3 +65,4 @@ export { default as BarChart } from './BarChart/BarChart.svelte';
|
|
|
65
65
|
export { default as PieChart } from './PieChart/PieChart.svelte';
|
|
66
66
|
export { default as SankeyChart } from './SankeyChart/SankeyChart.svelte';
|
|
67
67
|
export { validateInput } from './utils';
|
|
68
|
+
export { formatNumberIndian } from './_chart/format';
|