@juspay/svelte-ui-components 2.78.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.
- package/dist/ProportionBar/ProportionBar.svelte +211 -0
- package/dist/ProportionBar/ProportionBar.svelte.d.ts +23 -0
- package/dist/ProportionBar/properties.d.ts +34 -0
- package/dist/ProportionBar/properties.js +1 -0
- package/dist/StatCard/StatCard.svelte +269 -20
- package/dist/StatCard/properties.d.ts +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
61
|
-
<div class="statcard-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
204
|
+
{/if}
|
|
81
205
|
|
|
82
|
-
{#if
|
|
83
|
-
<div class="statcard-
|
|
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';
|