@juspay/svelte-ui-components 2.78.0 → 2.80.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.
@@ -0,0 +1,211 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ProportionBar — a horizontal stacked bar that visualises how a total is
4
+ * distributed across labelled segments. Renders proportional coloured bands in
5
+ * an SVG track with an optional legend. Each band's width is derived from its
6
+ * `value` relative to the sum of all segments; non-finite or negative values
7
+ * are treated as zero so the rendered widths always stay within 0–100%.
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * <ProportionBar
12
+ * segments={[
13
+ * { label: 'UPI', value: 480 },
14
+ * { label: 'Cards', value: 220 }
15
+ * ]}
16
+ * />
17
+ * ```
18
+ *
19
+ * @see docs/ProportionBar.md
20
+ */
21
+ import type { ProportionBarProperties, ProportionBarSegment } from './properties';
22
+
23
+ const DEFAULT_PALETTE = ['#8F49DE', '#FFC533', '#62D5C0', '#FF74CD', '#D1E7FF'];
24
+
25
+ let {
26
+ segments,
27
+ showLegend = true,
28
+ valueFormat,
29
+ trackHeight,
30
+ testId,
31
+ classes
32
+ }: ProportionBarProperties = $props();
33
+
34
+ const resolveColor = (segment: ProportionBarSegment, index: number): string => {
35
+ if (typeof segment.color === 'string' && segment.color.length > 0) {
36
+ return segment.color;
37
+ }
38
+ return DEFAULT_PALETTE[index % DEFAULT_PALETTE.length];
39
+ };
40
+
41
+ /** Reject negative or non-finite values so derived percentages stay within 0–100. */
42
+ const sanitizeValue = (value: number): number =>
43
+ Number.isFinite(value) && value > 0 ? value : 0;
44
+
45
+ const total = $derived(segments.reduce((sum, segment) => sum + sanitizeValue(segment.value), 0));
46
+
47
+ const computedSegments = $derived(
48
+ segments.map((segment, segmentIndex) => {
49
+ const safeValue = sanitizeValue(segment.value);
50
+ const percent = total > 0 ? (safeValue / total) * 100 : 0;
51
+ return {
52
+ label: segment.label,
53
+ value: safeValue,
54
+ percent,
55
+ color: resolveColor(segment, segmentIndex)
56
+ };
57
+ })
58
+ );
59
+
60
+ const defaultFormat = (absoluteValue: number, percent: number): string =>
61
+ `${absoluteValue.toLocaleString()} (${Math.round(percent)}%)`;
62
+
63
+ const formatValue = (absoluteValue: number, percent: number): string =>
64
+ (valueFormat ?? defaultFormat)(absoluteValue, percent);
65
+
66
+ /**
67
+ * Comma-joined "label: value (percent)" summary. Used as the SVG's accessible
68
+ * name when the legend is hidden, so screen-reader users still get the full
69
+ * breakdown even though the bands themselves are presentational.
70
+ */
71
+ const ariaSummary = $derived(
72
+ computedSegments
73
+ .map(
74
+ (computedSegment) =>
75
+ `${computedSegment.label}: ${formatValue(computedSegment.value, computedSegment.percent)}`
76
+ )
77
+ .join(', ')
78
+ );
79
+
80
+ /** Cumulative x offsets for SVG rect positions. */
81
+ const rectSegments = $derived(
82
+ computedSegments.reduce<{ x: number; width: number; color: string }[]>(
83
+ (acc, computedSegment) => {
84
+ const previousX = acc.length > 0 ? acc[acc.length - 1].x + acc[acc.length - 1].width : 0;
85
+ acc.push({ x: previousX, width: computedSegment.percent, color: computedSegment.color });
86
+ return acc;
87
+ },
88
+ []
89
+ )
90
+ );
91
+
92
+ const trackHeightStyle = $derived(
93
+ typeof trackHeight === 'string' && trackHeight.length > 0
94
+ ? `--proportion-bar-track-height: ${trackHeight};`
95
+ : ''
96
+ );
97
+ </script>
98
+
99
+ <div
100
+ class="proportion-bar {classes ?? ''}"
101
+ style={trackHeightStyle}
102
+ data-pw={typeof testId === 'string' ? testId : null}
103
+ >
104
+ <div class="proportion-bar-track">
105
+ <svg
106
+ viewBox="0 0 100 10"
107
+ preserveAspectRatio="none"
108
+ class="proportion-bar-svg"
109
+ role={showLegend ? null : 'img'}
110
+ aria-hidden={showLegend ? 'true' : null}
111
+ aria-label={showLegend ? null : ariaSummary}
112
+ >
113
+ {#each rectSegments as rectSegment, rectIndex (rectIndex)}
114
+ <rect
115
+ x={rectSegment.x}
116
+ y={0}
117
+ width={rectSegment.width}
118
+ height={10}
119
+ fill={rectSegment.color}
120
+ >
121
+ <title
122
+ >{computedSegments[rectIndex].label}: {formatValue(
123
+ computedSegments[rectIndex].value,
124
+ computedSegments[rectIndex].percent
125
+ )}</title
126
+ >
127
+ </rect>
128
+ {/each}
129
+ </svg>
130
+ </div>
131
+
132
+ {#if showLegend}
133
+ <ul class="proportion-bar-legend" aria-label="Segment breakdown">
134
+ {#each computedSegments as computedSegment, legendIndex (legendIndex)}
135
+ <li class="proportion-bar-legend-item">
136
+ <span
137
+ class="proportion-bar-swatch"
138
+ style="background: {computedSegment.color};"
139
+ aria-hidden="true"
140
+ ></span>
141
+ <span class="proportion-bar-legend-label">{computedSegment.label}</span>
142
+ <span class="proportion-bar-legend-value">
143
+ {formatValue(computedSegment.value, computedSegment.percent)}
144
+ </span>
145
+ </li>
146
+ {/each}
147
+ </ul>
148
+ {/if}
149
+ </div>
150
+
151
+ <style>
152
+ .proportion-bar {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: var(--proportion-bar-gap, 10px);
156
+ width: var(--proportion-bar-width, 100%);
157
+ }
158
+
159
+ .proportion-bar-track {
160
+ width: 100%;
161
+ height: var(--proportion-bar-track-height, 10px);
162
+ border-radius: var(--proportion-bar-track-border-radius, 4px);
163
+ overflow: hidden;
164
+ background: var(--proportion-bar-track-bg, #f0f0f0);
165
+ }
166
+
167
+ .proportion-bar-svg {
168
+ display: block;
169
+ width: 100%;
170
+ height: 100%;
171
+ }
172
+
173
+ .proportion-bar-legend {
174
+ list-style: none;
175
+ margin: 0;
176
+ padding: 0;
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: var(--proportion-bar-legend-gap, 6px);
180
+ }
181
+
182
+ .proportion-bar-legend-item {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: var(--proportion-bar-legend-item-gap, 8px);
186
+ }
187
+
188
+ .proportion-bar-swatch {
189
+ display: inline-block;
190
+ width: var(--proportion-bar-swatch-size, 10px);
191
+ height: var(--proportion-bar-swatch-size, 10px);
192
+ border-radius: var(--proportion-bar-swatch-border-radius, 2px);
193
+ flex-shrink: 0;
194
+ }
195
+
196
+ .proportion-bar-legend-label {
197
+ flex: 1;
198
+ font-size: var(--proportion-bar-legend-label-font-size, 13px);
199
+ font-weight: var(--proportion-bar-legend-label-font-weight, 400);
200
+ color: var(--proportion-bar-legend-label-color, #374151);
201
+ line-height: 1.4;
202
+ }
203
+
204
+ .proportion-bar-legend-value {
205
+ font-size: var(--proportion-bar-legend-value-font-size, 13px);
206
+ font-weight: var(--proportion-bar-legend-value-font-weight, 500);
207
+ color: var(--proportion-bar-legend-value-color, #111827);
208
+ line-height: 1.4;
209
+ white-space: nowrap;
210
+ }
211
+ </style>
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ProportionBar — a horizontal stacked bar that visualises how a total is
3
+ * distributed across labelled segments. Renders proportional coloured bands in
4
+ * an SVG track with an optional legend. Each band's width is derived from its
5
+ * `value` relative to the sum of all segments; non-finite or negative values
6
+ * are treated as zero so the rendered widths always stay within 0–100%.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <ProportionBar
11
+ * segments={[
12
+ * { label: 'UPI', value: 480 },
13
+ * { label: 'Cards', value: 220 }
14
+ * ]}
15
+ * />
16
+ * ```
17
+ *
18
+ * @see docs/ProportionBar.md
19
+ */
20
+ import type { ProportionBarProperties } from './properties';
21
+ declare const ProportionBar: import("svelte").Component<ProportionBarProperties, {}, "">;
22
+ type ProportionBar = ReturnType<typeof ProportionBar>;
23
+ export default ProportionBar;
@@ -0,0 +1,34 @@
1
+ export type ProportionBarSegment = {
2
+ /** Display label for this segment. */
3
+ label: string;
4
+ /** Absolute numeric value used to compute proportion. */
5
+ value: number;
6
+ /** Override fill color for this segment. Falls back to the default palette. */
7
+ color?: string;
8
+ };
9
+ export type ProportionBarProperties = MandatoryProportionBarProperties & OptionalProportionBarProperties;
10
+ export type MandatoryProportionBarProperties = {
11
+ /** Array of segments whose values define the proportions shown in the bar. */
12
+ segments: ProportionBarSegment[];
13
+ };
14
+ export type OptionalProportionBarProperties = {
15
+ /**
16
+ * Whether to render a legend list below the bar. Each legend item shows a
17
+ * colour swatch, the segment label, and the formatted value. Defaults to `true`.
18
+ */
19
+ showLegend?: boolean;
20
+ /**
21
+ * Custom formatter for the legend value column. Receives the absolute value and
22
+ * the computed percentage. Defaults to `"N (X%)"`.
23
+ */
24
+ valueFormat?: (value: number, percent: number) => string;
25
+ /**
26
+ * Height of the bar track (e.g. `"8px"`, `"12px"`). Also settable via the
27
+ * `--proportion-bar-track-height` CSS variable.
28
+ */
29
+ trackHeight?: string;
30
+ /** Test selector applied as the `data-pw` attribute on the root element. */
31
+ testId?: string;
32
+ /** Extra CSS class names appended to the root element. */
33
+ classes?: string;
34
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { StatCardProperties } from './properties';
3
+ import DeltaIndicator from '../DeltaIndicator/DeltaIndicator.svelte';
4
+ import Tooltip from '../Tooltip/Tooltip.svelte';
5
+ import CheckListItem from '../CheckListItem/CheckListItem.svelte';
3
6
 
4
7
  let {
5
8
  title,
@@ -9,9 +12,16 @@
9
12
  subtitle,
10
13
  footer,
11
14
  valueSnippet,
15
+ rows,
16
+ rowsDirection = 'column',
17
+ tooltip,
18
+ checkbox,
19
+ headerRight,
20
+ children,
12
21
  testId,
13
22
  classes,
14
- onclick
23
+ onclick,
24
+ onCheckboxChange
15
25
  }: StatCardProperties = $props();
16
26
 
17
27
  const isInteractive = $derived(typeof onclick === 'function');
@@ -34,6 +44,16 @@
34
44
  );
35
45
 
36
46
  const hasDelta = $derived(typeof delta === 'string' && delta.trim().length > 0);
47
+ const hasRows = $derived(Array.isArray(rows) && rows.length > 0);
48
+ const hasTitle = $derived(typeof title === 'string' && title.length > 0);
49
+
50
+ /**
51
+ * The header is rendered when ANY header content is present — not only a title.
52
+ * This keeps `checkbox` and `headerRight` visible on title-less cards.
53
+ */
54
+ const hasHeaderContent = $derived(
55
+ hasTitle || Boolean(checkbox) || typeof headerRight === 'function'
56
+ );
37
57
 
38
58
  const handleKeydown = (event: KeyboardEvent): void => {
39
59
  if (event.key === 'Enter' || event.key === ' ') {
@@ -45,6 +65,19 @@
45
65
  }
46
66
  }
47
67
  };
68
+
69
+ const handleCheckboxChange = (checked: boolean): void => {
70
+ onCheckboxChange?.(checked);
71
+ };
72
+
73
+ /**
74
+ * Keep header checkbox interaction from bubbling to the card's click/keydown
75
+ * handler — otherwise toggling the checkbox on an interactive card would also
76
+ * fire its `onclick` action.
77
+ */
78
+ const stopHeaderInteraction = (event: Event): void => {
79
+ event.stopPropagation();
80
+ };
48
81
  </script>
49
82
 
50
83
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
@@ -57,30 +90,122 @@
57
90
  onclick={isInteractive ? onclick : null}
58
91
  onkeydown={isInteractive ? handleKeydown : null}
59
92
  >
60
- {#if typeof title === 'string' && title.length > 0}
61
- <div class="statcard-title">{title}</div>
93
+ {#if hasHeaderContent}
94
+ <div class="statcard-header">
95
+ <div class="statcard-header-left">
96
+ {#if hasTitle}
97
+ {#if tooltip}
98
+ <Tooltip
99
+ text={tooltip.text}
100
+ position={tooltip.position ?? 'top'}
101
+ testId={tooltip.testId}
102
+ >
103
+ <div class="statcard-title statcard-title-tooltip">{title}</div>
104
+ </Tooltip>
105
+ {:else}
106
+ <div class="statcard-title">{title}</div>
107
+ {/if}
108
+ {/if}
109
+ {#if checkbox}
110
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
111
+ <div
112
+ class="statcard-header-checkbox"
113
+ onclick={stopHeaderInteraction}
114
+ onkeydown={stopHeaderInteraction}
115
+ >
116
+ <CheckListItem
117
+ text={checkbox.text}
118
+ checked={checkbox.checked ?? false}
119
+ onclick={handleCheckboxChange}
120
+ />
121
+ </div>
122
+ {/if}
123
+ </div>
124
+ {#if headerRight}
125
+ <div class="statcard-header-right">{@render headerRight()}</div>
126
+ {/if}
127
+ </div>
62
128
  {/if}
63
129
 
64
- <div class="statcard-value-row">
65
- {#if typeof valueSnippet === 'function'}
66
- <div class="statcard-value">{@render valueSnippet()}</div>
67
- {:else if typeof value === 'string' && value.length > 0}
68
- <div class="statcard-value">{value}</div>
69
- {/if}
130
+ {#if hasRows && rows}
131
+ <div class="statcard-rows" class:statcard-rows-horizontal={rowsDirection === 'row'}>
132
+ {#each rows as row, rowIndex (rowIndex)}
133
+ {#if rowIndex > 0}
134
+ <div class="statcard-row-divider"></div>
135
+ {/if}
136
+ <div class="statcard-row" data-pw={typeof row.testId === 'string' ? row.testId : null}>
137
+ {#if typeof row.heading === 'string' && row.heading.length > 0}
138
+ <div class="statcard-row-heading-wrap">
139
+ {#if row.tooltip}
140
+ <Tooltip
141
+ text={row.tooltip.text}
142
+ position={row.tooltip.position ?? 'top'}
143
+ testId={row.tooltip.testId}
144
+ >
145
+ <div class="statcard-row-heading statcard-row-heading-tooltip">{row.heading}</div>
146
+ </Tooltip>
147
+ {:else}
148
+ <div class="statcard-row-heading">{row.heading}</div>
149
+ {/if}
150
+ </div>
151
+ {/if}
152
+ <div class="statcard-row-value-line">
153
+ <div class="statcard-value">{row.value}</div>
154
+ {#if typeof row.change === 'number'}
155
+ <DeltaIndicator value={row.change} invertColors={row.invertChangeColors ?? false} />
156
+ {/if}
157
+ {#if typeof row.additionalContent === 'string' && row.additionalContent.length > 0}
158
+ <div class="statcard-row-additional">{row.additionalContent}</div>
159
+ {/if}
160
+ </div>
161
+ {#if Array.isArray(row.breakdown) && row.breakdown.length > 0}
162
+ {#if typeof row.breakdownHeading === 'string' && row.breakdownHeading.length > 0}
163
+ <div class="statcard-breakdown-heading">{row.breakdownHeading}</div>
164
+ {/if}
165
+ <div class="statcard-breakdown-grid">
166
+ {#each row.breakdown as breakdownItem, breakdownIndex (breakdownIndex)}
167
+ <div class="statcard-breakdown-item">
168
+ <div class="statcard-breakdown-label">{breakdownItem.label}</div>
169
+ <div class="statcard-breakdown-value">{breakdownItem.value}</div>
170
+ {#if typeof breakdownItem.change === 'number'}
171
+ <DeltaIndicator
172
+ value={breakdownItem.change}
173
+ invertColors={breakdownItem.invertChangeColors ?? false}
174
+ />
175
+ {/if}
176
+ </div>
177
+ {/each}
178
+ </div>
179
+ {/if}
180
+ </div>
181
+ {/each}
182
+ </div>
183
+ {:else}
184
+ <div class="statcard-value-row">
185
+ {#if typeof valueSnippet === 'function'}
186
+ <div class="statcard-value">{@render valueSnippet()}</div>
187
+ {:else if typeof value === 'string' && value.length > 0}
188
+ <div class="statcard-value">{value}</div>
189
+ {/if}
70
190
 
71
- {#if hasDelta}
72
- <div
73
- class="statcard-delta"
74
- class:statcard-delta-positive={resolvedDeltaPositive === true}
75
- class:statcard-delta-negative={resolvedDeltaPositive === false}
76
- >
77
- {delta}
78
- </div>
191
+ {#if hasDelta}
192
+ <div
193
+ class="statcard-delta"
194
+ class:statcard-delta-positive={resolvedDeltaPositive === true}
195
+ class:statcard-delta-negative={resolvedDeltaPositive === false}
196
+ >
197
+ {delta}
198
+ </div>
199
+ {/if}
200
+ </div>
201
+
202
+ {#if typeof subtitle === 'string' && subtitle.length > 0}
203
+ <div class="statcard-subtitle">{subtitle}</div>
79
204
  {/if}
80
- </div>
205
+ {/if}
81
206
 
82
- {#if typeof subtitle === 'string' && subtitle.length > 0}
83
- <div class="statcard-subtitle">{subtitle}</div>
207
+ {#if children}
208
+ <div class="statcard-children">{@render children()}</div>
84
209
  {/if}
85
210
 
86
211
  {#if typeof footer === 'function'}
@@ -116,6 +241,33 @@
116
241
  outline-offset: var(--statcard-focus-outline-offset, 2px);
117
242
  }
118
243
 
244
+ .statcard-header {
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: space-between;
248
+ gap: var(--statcard-header-gap, 8px);
249
+ }
250
+
251
+ .statcard-header-left {
252
+ display: flex;
253
+ align-items: center;
254
+ gap: var(--statcard-header-left-gap, 8px);
255
+ flex: 1;
256
+ min-width: 0;
257
+ }
258
+
259
+ .statcard-header-right {
260
+ display: flex;
261
+ align-items: center;
262
+ flex-shrink: 0;
263
+ }
264
+
265
+ /* Transparent wrapper: keeps the checkbox in the event path (to stop bubbling to
266
+ the card action) without altering the header layout. */
267
+ .statcard-header-checkbox {
268
+ display: contents;
269
+ }
270
+
119
271
  .statcard-title {
120
272
  font-size: var(--statcard-title-font-size, 12px);
121
273
  font-weight: var(--statcard-title-font-weight, 500);
@@ -126,6 +278,12 @@
126
278
  text-overflow: ellipsis;
127
279
  }
128
280
 
281
+ .statcard-title-tooltip {
282
+ cursor: default;
283
+ text-decoration: underline dotted;
284
+ text-underline-offset: 2px;
285
+ }
286
+
129
287
  .statcard-value-row {
130
288
  display: flex;
131
289
  align-items: var(--statcard-value-row-align, baseline);
@@ -162,6 +320,117 @@
162
320
  line-height: var(--statcard-subtitle-line-height, 1.4);
163
321
  }
164
322
 
323
+ /* Multi-row layout */
324
+ .statcard-rows {
325
+ display: flex;
326
+ flex-direction: column;
327
+ gap: var(--statcard-rows-gap, 0px);
328
+ }
329
+
330
+ .statcard-row-divider {
331
+ height: var(--statcard-row-divider-height, 1px);
332
+ background: var(--statcard-row-divider-color, #e5e7eb);
333
+ margin: var(--statcard-row-divider-margin, 8px 0);
334
+ }
335
+
336
+ /* Horizontal layout: lay the metric sections side by side with vertical
337
+ dividers. Each section flexes to share the available width equally. */
338
+ .statcard-rows-horizontal {
339
+ flex-direction: row;
340
+ }
341
+
342
+ .statcard-rows-horizontal .statcard-row {
343
+ flex: 1;
344
+ min-width: 0;
345
+ }
346
+
347
+ .statcard-rows-horizontal .statcard-row-divider {
348
+ align-self: stretch;
349
+ flex-shrink: 0;
350
+ width: var(--statcard-row-divider-height, 1px);
351
+ height: auto;
352
+ margin: var(--statcard-row-divider-margin-horizontal, 0 16px);
353
+ }
354
+
355
+ .statcard-row {
356
+ display: flex;
357
+ flex-direction: column;
358
+ gap: var(--statcard-row-gap, 4px);
359
+ }
360
+
361
+ .statcard-row-heading-wrap {
362
+ display: flex;
363
+ align-items: center;
364
+ }
365
+
366
+ .statcard-row-heading {
367
+ font-size: var(--statcard-row-heading-font-size, 12px);
368
+ font-weight: var(--statcard-row-heading-font-weight, 500);
369
+ color: var(--statcard-row-heading-color, #6b7280);
370
+ line-height: var(--statcard-row-heading-line-height, 1.4);
371
+ }
372
+
373
+ .statcard-row-heading-tooltip {
374
+ cursor: default;
375
+ text-decoration: underline dotted;
376
+ text-underline-offset: 2px;
377
+ }
378
+
379
+ .statcard-row-value-line {
380
+ display: flex;
381
+ align-items: baseline;
382
+ gap: var(--statcard-row-value-gap, 8px);
383
+ flex-wrap: wrap;
384
+ }
385
+
386
+ .statcard-row-additional {
387
+ font-size: var(--statcard-row-additional-font-size, 12px);
388
+ font-weight: var(--statcard-row-additional-font-weight, 400);
389
+ color: var(--statcard-row-additional-color, #9ca3af);
390
+ line-height: 1.4;
391
+ }
392
+
393
+ /* Breakdown grid */
394
+ .statcard-breakdown-heading {
395
+ font-size: var(--statcard-breakdown-heading-font-size, 11px);
396
+ font-weight: var(--statcard-breakdown-heading-font-weight, 600);
397
+ color: var(--statcard-breakdown-heading-color, #6b7280);
398
+ letter-spacing: 0.04em;
399
+ text-transform: uppercase;
400
+ margin-top: var(--statcard-breakdown-heading-margin-top, 6px);
401
+ }
402
+
403
+ .statcard-breakdown-grid {
404
+ display: grid;
405
+ grid-template-columns: repeat(auto-fill, minmax(var(--statcard-breakdown-col-min, 100px), 1fr));
406
+ gap: var(--statcard-breakdown-gap, 8px);
407
+ margin-top: var(--statcard-breakdown-margin-top, 4px);
408
+ }
409
+
410
+ .statcard-breakdown-item {
411
+ display: flex;
412
+ flex-direction: column;
413
+ gap: var(--statcard-breakdown-item-gap, 2px);
414
+ }
415
+
416
+ .statcard-breakdown-label {
417
+ font-size: var(--statcard-breakdown-label-font-size, 11px);
418
+ font-weight: var(--statcard-breakdown-label-font-weight, 400);
419
+ color: var(--statcard-breakdown-label-color, #9ca3af);
420
+ line-height: 1.4;
421
+ }
422
+
423
+ .statcard-breakdown-value {
424
+ font-size: var(--statcard-breakdown-value-font-size, 14px);
425
+ font-weight: var(--statcard-breakdown-value-font-weight, 600);
426
+ color: var(--statcard-breakdown-value-color, inherit);
427
+ line-height: 1.2;
428
+ }
429
+
430
+ .statcard-children {
431
+ margin-top: var(--statcard-children-margin-top, 4px);
432
+ }
433
+
165
434
  .statcard-footer {
166
435
  margin-top: var(--statcard-footer-margin-top, 8px);
167
436
  padding-top: var(--statcard-footer-padding-top, 8px);
@@ -1,6 +1,50 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  export type StatCardProperties = OptionalStatCardProperties & StatCardEventProperties;
3
3
  export type MandatoryStatCardProperties = Record<string, never>;
4
+ export type StatCardTooltip = {
5
+ text: string;
6
+ position?: 'top' | 'bottom' | 'left' | 'right';
7
+ testId?: string;
8
+ };
9
+ export type StatCardBreakdownItem = {
10
+ label: string;
11
+ value: string;
12
+ change?: number;
13
+ invertChangeColors?: boolean;
14
+ };
15
+ /**
16
+ * A single metric row inside a multi-row StatCard.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const row: StatCardRow = {
21
+ * heading: 'Gross Revenue',
22
+ * value: '₹12.4Cr',
23
+ * change: 8.2,
24
+ * tooltip: { text: 'Revenue before returns' }
25
+ * };
26
+ * ```
27
+ */
28
+ export type StatCardRow = {
29
+ /** The metric value string for this row (e.g. "₹1.23Cr", "98.4%"). */
30
+ value: string;
31
+ /** Optional row-level heading label. */
32
+ heading?: string;
33
+ /** Numeric change for the delta indicator. */
34
+ change?: number;
35
+ /** Invert delta colors for lower-is-better metrics on this row. */
36
+ invertChangeColors?: boolean;
37
+ /** Additional descriptive text rendered after the delta. */
38
+ additionalContent?: string;
39
+ /** Tooltip shown on the row heading. */
40
+ tooltip?: StatCardTooltip;
41
+ /** Heading rendered above the breakdown grid. */
42
+ breakdownHeading?: string;
43
+ /** Breakdown items rendered in a grid below the row value. */
44
+ breakdown?: StatCardBreakdownItem[];
45
+ /** Test selector for the row root element. */
46
+ testId?: string;
47
+ };
4
48
  export type OptionalStatCardProperties = {
5
49
  /** Card heading label. */
6
50
  title?: string;
@@ -16,6 +60,28 @@ export type OptionalStatCardProperties = {
16
60
  footer?: Snippet;
17
61
  /** Replaces the string `value` with a custom snippet for advanced value rendering. */
18
62
  valueSnippet?: Snippet;
63
+ /**
64
+ * Multiple metric rows. When provided, replaces the single value/delta row with
65
+ * a column of rows separated by dividers.
66
+ */
67
+ rows?: StatCardRow[];
68
+ /**
69
+ * Layout direction for `rows`. `'column'` (default) stacks rows vertically with
70
+ * horizontal dividers; `'row'` lays the sections side by side with vertical
71
+ * dividers, each section flexing to share the width equally.
72
+ */
73
+ rowsDirection?: 'column' | 'row';
74
+ /** Tooltip shown on the card title. */
75
+ tooltip?: StatCardTooltip;
76
+ /** Checkbox rendered next to the title. */
77
+ checkbox?: {
78
+ text: string;
79
+ checked?: boolean;
80
+ };
81
+ /** Snippet rendered at the right edge of the header row. */
82
+ headerRight?: Snippet;
83
+ /** Snippet rendered inside the card body, after any rows. */
84
+ children?: Snippet;
19
85
  /** Renders as `data-pw` on the root element for Playwright test selection. */
20
86
  testId?: string;
21
87
  /** Extra CSS class names appended to the root element. */
@@ -24,4 +90,6 @@ export type OptionalStatCardProperties = {
24
90
  export type StatCardEventProperties = {
25
91
  /** Makes the card interactive: adds `role="button"`, `tabindex=0`, and wires click/Enter/Space. */
26
92
  onclick?: (event: MouseEvent) => void;
93
+ /** Fired when the header checkbox changes. */
94
+ onCheckboxChange?: (checked: boolean) => void;
27
95
  };
package/dist/index.d.ts CHANGED
@@ -71,6 +71,7 @@ export { default as StatCard } from './StatCard/StatCard.svelte';
71
71
  export { default as DeltaIndicator } from './DeltaIndicator/DeltaIndicator.svelte';
72
72
  export { default as DualAxisBarChart } from './DualAxisBarChart/DualAxisBarChart.svelte';
73
73
  export { default as FunnelChart } from './FunnelChart/FunnelChart.svelte';
74
+ export { default as ProportionBar } from './ProportionBar/ProportionBar.svelte';
74
75
  export type * from './Button/properties';
75
76
  export type * from './Modal/properties';
76
77
  export type * from './Input/properties';
@@ -138,6 +139,7 @@ export type * from './StatCard/properties';
138
139
  export type * from './DeltaIndicator/properties';
139
140
  export type * from './DualAxisBarChart/properties';
140
141
  export type * from './FunnelChart/properties';
142
+ export type * from './ProportionBar/properties';
141
143
  export type * from './_chart/highlight';
142
144
  export { validateInput } from './utils';
143
145
  export { formatNumberIndian } from './_chart/format';
package/dist/index.js CHANGED
@@ -71,5 +71,6 @@ export { default as StatCard } from './StatCard/StatCard.svelte';
71
71
  export { default as DeltaIndicator } from './DeltaIndicator/DeltaIndicator.svelte';
72
72
  export { default as DualAxisBarChart } from './DualAxisBarChart/DualAxisBarChart.svelte';
73
73
  export { default as FunnelChart } from './FunnelChart/FunnelChart.svelte';
74
+ export { default as ProportionBar } from './ProportionBar/ProportionBar.svelte';
74
75
  export { validateInput } from './utils';
75
76
  export { formatNumberIndian } from './_chart/format';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.78.0",
3
+ "version": "2.80.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",