@juspay/svelte-ui-components 2.77.0 → 2.79.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.
@@ -9,7 +9,10 @@
9
9
  showDots = false,
10
10
  isScrollableLast = false,
11
11
  onkeydown,
12
- classes
12
+ classes,
13
+ testId,
14
+ dotsWrapperTestId,
15
+ dotTestId
13
16
  }: CarouselProperties = $props();
14
17
 
15
18
  let slidesDiv: HTMLDivElement | null = $state(null);
@@ -138,7 +141,10 @@
138
141
  });
139
142
  </script>
140
143
 
141
- <div class="carousel-container {classes ?? ''}">
144
+ <div
145
+ class="carousel-container {classes ?? ''}"
146
+ data-pw={typeof testId === 'string' ? testId : null}
147
+ >
142
148
  {#if views.length > 0}
143
149
  <div class="carousel" bind:this={carouselDiv}>
144
150
  <div class="slidesDiv" bind:this={slidesDiv}>
@@ -155,7 +161,10 @@
155
161
  </div>
156
162
  {/if}
157
163
  {#if showDots}
158
- <div class="dots-wrapper">
164
+ <div
165
+ class="dots-wrapper"
166
+ data-pw={typeof dotsWrapperTestId === 'string' ? dotsWrapperTestId : null}
167
+ >
159
168
  <!-- eslint-disable-next-line -->
160
169
  {#each views as _, index}
161
170
  <div
@@ -163,6 +172,7 @@
163
172
  onclick={() => moveSlideToIndex(index)}
164
173
  {onkeydown}
165
174
  role="none"
175
+ data-pw={typeof dotTestId === 'string' ? `${dotTestId}-${index + 1}` : null}
166
176
  ></div>
167
177
  {/each}
168
178
  </div>
@@ -13,6 +13,9 @@ export type OptionalCarouselProperties = {
13
13
  showDots?: boolean;
14
14
  isScrollableLast?: boolean;
15
15
  classes?: string;
16
+ testId?: string;
17
+ dotsWrapperTestId?: string;
18
+ dotTestId?: string;
16
19
  };
17
20
  export type CarouselEventProperties = {
18
21
  onkeydown?: (event: KeyboardEvent) => void;
@@ -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,15 @@
9
12
  subtitle,
10
13
  footer,
11
14
  valueSnippet,
15
+ rows,
16
+ tooltip,
17
+ checkbox,
18
+ headerRight,
19
+ children,
12
20
  testId,
13
21
  classes,
14
- onclick
22
+ onclick,
23
+ onCheckboxChange
15
24
  }: StatCardProperties = $props();
16
25
 
17
26
  const isInteractive = $derived(typeof onclick === 'function');
@@ -34,6 +43,16 @@
34
43
  );
35
44
 
36
45
  const hasDelta = $derived(typeof delta === 'string' && delta.trim().length > 0);
46
+ const hasRows = $derived(Array.isArray(rows) && rows.length > 0);
47
+ const hasTitle = $derived(typeof title === 'string' && title.length > 0);
48
+
49
+ /**
50
+ * The header is rendered when ANY header content is present — not only a title.
51
+ * This keeps `checkbox` and `headerRight` visible on title-less cards.
52
+ */
53
+ const hasHeaderContent = $derived(
54
+ hasTitle || Boolean(checkbox) || typeof headerRight === 'function'
55
+ );
37
56
 
38
57
  const handleKeydown = (event: KeyboardEvent): void => {
39
58
  if (event.key === 'Enter' || event.key === ' ') {
@@ -45,6 +64,19 @@
45
64
  }
46
65
  }
47
66
  };
67
+
68
+ const handleCheckboxChange = (checked: boolean): void => {
69
+ onCheckboxChange?.(checked);
70
+ };
71
+
72
+ /**
73
+ * Keep header checkbox interaction from bubbling to the card's click/keydown
74
+ * handler — otherwise toggling the checkbox on an interactive card would also
75
+ * fire its `onclick` action.
76
+ */
77
+ const stopHeaderInteraction = (event: Event): void => {
78
+ event.stopPropagation();
79
+ };
48
80
  </script>
49
81
 
50
82
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
@@ -57,30 +89,122 @@
57
89
  onclick={isInteractive ? onclick : null}
58
90
  onkeydown={isInteractive ? handleKeydown : null}
59
91
  >
60
- {#if typeof title === 'string' && title.length > 0}
61
- <div class="statcard-title">{title}</div>
92
+ {#if hasHeaderContent}
93
+ <div class="statcard-header">
94
+ <div class="statcard-header-left">
95
+ {#if hasTitle}
96
+ {#if tooltip}
97
+ <Tooltip
98
+ text={tooltip.text}
99
+ position={tooltip.position ?? 'top'}
100
+ testId={tooltip.testId}
101
+ >
102
+ <div class="statcard-title statcard-title-tooltip">{title}</div>
103
+ </Tooltip>
104
+ {:else}
105
+ <div class="statcard-title">{title}</div>
106
+ {/if}
107
+ {/if}
108
+ {#if checkbox}
109
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
110
+ <div
111
+ class="statcard-header-checkbox"
112
+ onclick={stopHeaderInteraction}
113
+ onkeydown={stopHeaderInteraction}
114
+ >
115
+ <CheckListItem
116
+ text={checkbox.text}
117
+ checked={checkbox.checked ?? false}
118
+ onclick={handleCheckboxChange}
119
+ />
120
+ </div>
121
+ {/if}
122
+ </div>
123
+ {#if headerRight}
124
+ <div class="statcard-header-right">{@render headerRight()}</div>
125
+ {/if}
126
+ </div>
62
127
  {/if}
63
128
 
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}
129
+ {#if hasRows && rows}
130
+ <div class="statcard-rows">
131
+ {#each rows as row, rowIndex (rowIndex)}
132
+ {#if rowIndex > 0}
133
+ <div class="statcard-row-divider"></div>
134
+ {/if}
135
+ <div class="statcard-row" data-pw={typeof row.testId === 'string' ? row.testId : null}>
136
+ {#if typeof row.heading === 'string' && row.heading.length > 0}
137
+ <div class="statcard-row-heading-wrap">
138
+ {#if row.tooltip}
139
+ <Tooltip
140
+ text={row.tooltip.text}
141
+ position={row.tooltip.position ?? 'top'}
142
+ testId={row.tooltip.testId}
143
+ >
144
+ <div class="statcard-row-heading statcard-row-heading-tooltip">{row.heading}</div>
145
+ </Tooltip>
146
+ {:else}
147
+ <div class="statcard-row-heading">{row.heading}</div>
148
+ {/if}
149
+ </div>
150
+ {/if}
151
+ <div class="statcard-row-value-line">
152
+ <div class="statcard-value">{row.value}</div>
153
+ {#if typeof row.change === 'number'}
154
+ <DeltaIndicator value={row.change} invertColors={row.invertChangeColors ?? false} />
155
+ {/if}
156
+ {#if typeof row.additionalContent === 'string' && row.additionalContent.length > 0}
157
+ <div class="statcard-row-additional">{row.additionalContent}</div>
158
+ {/if}
159
+ </div>
160
+ {#if Array.isArray(row.breakdown) && row.breakdown.length > 0}
161
+ {#if typeof row.breakdownHeading === 'string' && row.breakdownHeading.length > 0}
162
+ <div class="statcard-breakdown-heading">{row.breakdownHeading}</div>
163
+ {/if}
164
+ <div class="statcard-breakdown-grid">
165
+ {#each row.breakdown as breakdownItem, breakdownIndex (breakdownIndex)}
166
+ <div class="statcard-breakdown-item">
167
+ <div class="statcard-breakdown-label">{breakdownItem.label}</div>
168
+ <div class="statcard-breakdown-value">{breakdownItem.value}</div>
169
+ {#if typeof breakdownItem.change === 'number'}
170
+ <DeltaIndicator
171
+ value={breakdownItem.change}
172
+ invertColors={breakdownItem.invertChangeColors ?? false}
173
+ />
174
+ {/if}
175
+ </div>
176
+ {/each}
177
+ </div>
178
+ {/if}
179
+ </div>
180
+ {/each}
181
+ </div>
182
+ {:else}
183
+ <div class="statcard-value-row">
184
+ {#if typeof valueSnippet === 'function'}
185
+ <div class="statcard-value">{@render valueSnippet()}</div>
186
+ {:else if typeof value === 'string' && value.length > 0}
187
+ <div class="statcard-value">{value}</div>
188
+ {/if}
70
189
 
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>
190
+ {#if hasDelta}
191
+ <div
192
+ class="statcard-delta"
193
+ class:statcard-delta-positive={resolvedDeltaPositive === true}
194
+ class:statcard-delta-negative={resolvedDeltaPositive === false}
195
+ >
196
+ {delta}
197
+ </div>
198
+ {/if}
199
+ </div>
200
+
201
+ {#if typeof subtitle === 'string' && subtitle.length > 0}
202
+ <div class="statcard-subtitle">{subtitle}</div>
79
203
  {/if}
80
- </div>
204
+ {/if}
81
205
 
82
- {#if typeof subtitle === 'string' && subtitle.length > 0}
83
- <div class="statcard-subtitle">{subtitle}</div>
206
+ {#if children}
207
+ <div class="statcard-children">{@render children()}</div>
84
208
  {/if}
85
209
 
86
210
  {#if typeof footer === 'function'}
@@ -116,6 +240,33 @@
116
240
  outline-offset: var(--statcard-focus-outline-offset, 2px);
117
241
  }
118
242
 
243
+ .statcard-header {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: space-between;
247
+ gap: var(--statcard-header-gap, 8px);
248
+ }
249
+
250
+ .statcard-header-left {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: var(--statcard-header-left-gap, 8px);
254
+ flex: 1;
255
+ min-width: 0;
256
+ }
257
+
258
+ .statcard-header-right {
259
+ display: flex;
260
+ align-items: center;
261
+ flex-shrink: 0;
262
+ }
263
+
264
+ /* Transparent wrapper: keeps the checkbox in the event path (to stop bubbling to
265
+ the card action) without altering the header layout. */
266
+ .statcard-header-checkbox {
267
+ display: contents;
268
+ }
269
+
119
270
  .statcard-title {
120
271
  font-size: var(--statcard-title-font-size, 12px);
121
272
  font-weight: var(--statcard-title-font-weight, 500);
@@ -126,6 +277,12 @@
126
277
  text-overflow: ellipsis;
127
278
  }
128
279
 
280
+ .statcard-title-tooltip {
281
+ cursor: default;
282
+ text-decoration: underline dotted;
283
+ text-underline-offset: 2px;
284
+ }
285
+
129
286
  .statcard-value-row {
130
287
  display: flex;
131
288
  align-items: var(--statcard-value-row-align, baseline);
@@ -162,6 +319,98 @@
162
319
  line-height: var(--statcard-subtitle-line-height, 1.4);
163
320
  }
164
321
 
322
+ /* Multi-row layout */
323
+ .statcard-rows {
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: var(--statcard-rows-gap, 0px);
327
+ }
328
+
329
+ .statcard-row-divider {
330
+ height: var(--statcard-row-divider-height, 1px);
331
+ background: var(--statcard-row-divider-color, #e5e7eb);
332
+ margin: var(--statcard-row-divider-margin, 8px 0);
333
+ }
334
+
335
+ .statcard-row {
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: var(--statcard-row-gap, 4px);
339
+ }
340
+
341
+ .statcard-row-heading-wrap {
342
+ display: flex;
343
+ align-items: center;
344
+ }
345
+
346
+ .statcard-row-heading {
347
+ font-size: var(--statcard-row-heading-font-size, 12px);
348
+ font-weight: var(--statcard-row-heading-font-weight, 500);
349
+ color: var(--statcard-row-heading-color, #6b7280);
350
+ line-height: var(--statcard-row-heading-line-height, 1.4);
351
+ }
352
+
353
+ .statcard-row-heading-tooltip {
354
+ cursor: default;
355
+ text-decoration: underline dotted;
356
+ text-underline-offset: 2px;
357
+ }
358
+
359
+ .statcard-row-value-line {
360
+ display: flex;
361
+ align-items: baseline;
362
+ gap: var(--statcard-row-value-gap, 8px);
363
+ flex-wrap: wrap;
364
+ }
365
+
366
+ .statcard-row-additional {
367
+ font-size: var(--statcard-row-additional-font-size, 12px);
368
+ font-weight: var(--statcard-row-additional-font-weight, 400);
369
+ color: var(--statcard-row-additional-color, #9ca3af);
370
+ line-height: 1.4;
371
+ }
372
+
373
+ /* Breakdown grid */
374
+ .statcard-breakdown-heading {
375
+ font-size: var(--statcard-breakdown-heading-font-size, 11px);
376
+ font-weight: var(--statcard-breakdown-heading-font-weight, 600);
377
+ color: var(--statcard-breakdown-heading-color, #6b7280);
378
+ letter-spacing: 0.04em;
379
+ text-transform: uppercase;
380
+ margin-top: var(--statcard-breakdown-heading-margin-top, 6px);
381
+ }
382
+
383
+ .statcard-breakdown-grid {
384
+ display: grid;
385
+ grid-template-columns: repeat(auto-fill, minmax(var(--statcard-breakdown-col-min, 100px), 1fr));
386
+ gap: var(--statcard-breakdown-gap, 8px);
387
+ margin-top: var(--statcard-breakdown-margin-top, 4px);
388
+ }
389
+
390
+ .statcard-breakdown-item {
391
+ display: flex;
392
+ flex-direction: column;
393
+ gap: var(--statcard-breakdown-item-gap, 2px);
394
+ }
395
+
396
+ .statcard-breakdown-label {
397
+ font-size: var(--statcard-breakdown-label-font-size, 11px);
398
+ font-weight: var(--statcard-breakdown-label-font-weight, 400);
399
+ color: var(--statcard-breakdown-label-color, #9ca3af);
400
+ line-height: 1.4;
401
+ }
402
+
403
+ .statcard-breakdown-value {
404
+ font-size: var(--statcard-breakdown-value-font-size, 14px);
405
+ font-weight: var(--statcard-breakdown-value-font-weight, 600);
406
+ color: var(--statcard-breakdown-value-color, inherit);
407
+ line-height: 1.2;
408
+ }
409
+
410
+ .statcard-children {
411
+ margin-top: var(--statcard-children-margin-top, 4px);
412
+ }
413
+
165
414
  .statcard-footer {
166
415
  margin-top: var(--statcard-footer-margin-top, 8px);
167
416
  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,22 @@ 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
+ /** Tooltip shown on the card title. */
69
+ tooltip?: StatCardTooltip;
70
+ /** Checkbox rendered next to the title. */
71
+ checkbox?: {
72
+ text: string;
73
+ checked?: boolean;
74
+ };
75
+ /** Snippet rendered at the right edge of the header row. */
76
+ headerRight?: Snippet;
77
+ /** Snippet rendered inside the card body, after any rows. */
78
+ children?: Snippet;
19
79
  /** Renders as `data-pw` on the root element for Playwright test selection. */
20
80
  testId?: string;
21
81
  /** Extra CSS class names appended to the root element. */
@@ -24,4 +84,6 @@ export type OptionalStatCardProperties = {
24
84
  export type StatCardEventProperties = {
25
85
  /** Makes the card interactive: adds `role="button"`, `tabindex=0`, and wires click/Enter/Space. */
26
86
  onclick?: (event: MouseEvent) => void;
87
+ /** Fired when the header checkbox changes. */
88
+ onCheckboxChange?: (checked: boolean) => void;
27
89
  };
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.77.0",
3
+ "version": "2.79.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",