@dax-ui/kpi-card 0.0.0-stage → 0.4.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/CHANGELOG.md +22 -0
- package/LICENSE +12 -0
- package/README.md +134 -2
- package/lib/index.d.ts +217 -0
- package/lib/index.js +514 -0
- package/lib/index.js.map +1 -0
- package/package.json +57 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @dax-ui/kpi-card
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @dax-ui/tokens@0.4.0
|
|
8
|
+
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 3b106ed: New `@dax-ui/kpi-card` package: `KpiCard` dashboard figures that count up, with an icon,
|
|
14
|
+
trend, tag and caption, a sparkline that draws itself in, and six variants (`glass`,
|
|
15
|
+
`solid`, `elevated`, `tinted`, `outlined`, `accent`) in five colors. `KpiCardGrid` lays
|
|
16
|
+
cards out and staggers their entrance, `KpiSplitBar` splits a total into animated parts,
|
|
17
|
+
and `useCountUp`, `KpiSparkline`, `formatCount`, `formatDuration` and `formatShare` are
|
|
18
|
+
exported. `@dax-ui/react` now includes it.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- @dax-ui/tokens@0.3.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (c) 2026 Digital Auxilius. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its source code are proprietary. They are made publicly available
|
|
4
|
+
for viewing and installation only. No license is granted to use, copy, modify, merge,
|
|
5
|
+
publish, distribute, sublicense or sell copies of this software, in whole or in part,
|
|
6
|
+
except under a separate written agreement with Digital Auxilius.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
9
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
10
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
11
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF OR
|
|
12
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dax-ui/kpi-card
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Dashboard figures that animate: the number counts up, cards slide in one after another,
|
|
4
|
+
sparklines draw themselves, and split bars grow into place. Six looks (`glass`, `solid`,
|
|
5
|
+
`elevated`, `tinted`, `outlined`, `accent`) in five colors. Every motion is off for
|
|
6
|
+
viewers who ask for reduced motion.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @dax-ui/kpi-card
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
MUI and Emotion are included; the app only needs React 18 or 19.
|
|
15
|
+
|
|
16
|
+
One-time setup per app (skip if the app already uses dax-ui): install
|
|
17
|
+
[`@dax-ui/theme`](https://www.npmjs.com/package/@dax-ui/theme) and wrap the app in its `UIProvider`.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @dax-ui/theme
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { formatDuration, formatShare, KpiCard, KpiCardGrid, KpiSplitBar } from '@dax-ui/kpi-card';
|
|
27
|
+
import { Clock, Phone, PhoneCall } from 'lucide-react';
|
|
28
|
+
|
|
29
|
+
<KpiCardGrid>
|
|
30
|
+
{/* The headline figure: solid, with the whole split into its parts. */}
|
|
31
|
+
<KpiCard variant="solid" label="Total Calls" value={3482} icon={<Phone />} sparkline={lastTenDays}>
|
|
32
|
+
<KpiSplitBar
|
|
33
|
+
segments={[
|
|
34
|
+
{ label: 'AI Calls', value: 2190 },
|
|
35
|
+
{ label: 'Manual Calls', value: 1292 },
|
|
36
|
+
]}
|
|
37
|
+
/>
|
|
38
|
+
</KpiCard>
|
|
39
|
+
|
|
40
|
+
<KpiCard
|
|
41
|
+
label="Connected Calls"
|
|
42
|
+
value={1874}
|
|
43
|
+
icon={<PhoneCall />}
|
|
44
|
+
trend={{ value: '12%', direction: 'up' }}
|
|
45
|
+
tag={formatShare(1874, 3482)}
|
|
46
|
+
tagTone="success"
|
|
47
|
+
caption="of total calls"
|
|
48
|
+
sparkline={lastTenDays}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<KpiCard variant="accent" color="warning" label="Total Talk Time" value={93420} format={formatDuration} icon={<Clock />} />
|
|
52
|
+
</KpiCardGrid>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Variants
|
|
56
|
+
|
|
57
|
+
| `variant` | Look | On hover |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `glass` (default) | Frosted and translucent; blurs what's behind it | |
|
|
60
|
+
| `solid` | Filled with `color`, white text. For the headline figure | |
|
|
61
|
+
| `elevated` | Plain surface with a box shadow | Lifts, shadow deepens |
|
|
62
|
+
| `tinted` | Background washed with `color` | Wash deepens |
|
|
63
|
+
| `outlined` | Thin border, no shadow | Border takes `color` |
|
|
64
|
+
| `accent` | Plain surface with a `color` line along the top that draws in | Line thickens |
|
|
65
|
+
|
|
66
|
+
`color` is `brand`, `success`, `warning`, `error` or `info`, and also recolors the figure,
|
|
67
|
+
icon and sparkline.
|
|
68
|
+
|
|
69
|
+
### Changing figures
|
|
70
|
+
|
|
71
|
+
When `value` changes, the figure counts from the old number to the new one rather than
|
|
72
|
+
from zero, so a date filter nudges the cards. While data loads, pass `loading`: the
|
|
73
|
+
figure holds and pulses, the trend and tag fade out, and any `KpiSplitBar` empties.
|
|
74
|
+
Screen readers get the final figure once, not every frame.
|
|
75
|
+
|
|
76
|
+
### Formatting
|
|
77
|
+
|
|
78
|
+
`format` turns the figure into text on every frame. `formatCount` (the default) adds
|
|
79
|
+
thousands separators; `formatDuration` turns seconds into two units with the second one
|
|
80
|
+
smaller (`25h 57m`); `formatShare(part, whole)` gives a percentage for a `tag`. For
|
|
81
|
+
fractional figures, set `decimals` so the count-up keeps them:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<KpiCard label="Connection Rate" value={54.3} decimals={1} format={(v) => `${v.toFixed(1)}%`} />
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`useCountUp(value, options)` and `KpiSparkline` are exported for building your own cards.
|
|
88
|
+
|
|
89
|
+
## Props
|
|
90
|
+
|
|
91
|
+
### KpiCard
|
|
92
|
+
|
|
93
|
+
<!-- props:start KpiCard -->
|
|
94
|
+
| Prop | Type | Default | Description |
|
|
95
|
+
| --- | --- | --- | --- |
|
|
96
|
+
| `label` *(required)* | `ReactNode` | | What the figure measures, e.g. "Connected Calls". |
|
|
97
|
+
| `value` *(required)* | `number` | | The figure. Counted up to on mount, and from the old figure to the new one when it changes. |
|
|
98
|
+
| `animate` | `boolean` | `true` | Animates the card: it slides in, the figure counts up and the sparkline draws itself. Always off for viewers who ask for reduced motion. |
|
|
99
|
+
| `caption` | `ReactNode` | | Muted text after the tag, e.g. "of total calls". |
|
|
100
|
+
| `children` | `ReactNode` | | Extra content under the figure, e.g. a `KpiSplitBar`. Replaces the footer unless `tag` or `caption` is set. |
|
|
101
|
+
| `color` | `"brand" \| "success" \| "warning" \| "error" \| "info"` | `brand` | The card's color: the figure, icon, sparkline, and the fill, tint or line of the `solid`, `tinted` and `accent` variants. |
|
|
102
|
+
| `countUpDuration` | `number` | `900` | How long the count-up takes, in ms. |
|
|
103
|
+
| `decimals` | `number` | `0` | Decimal places the count-up keeps, for fractional figures like rates. |
|
|
104
|
+
| `enterDelay` | `number` | | Delay before the card slides in, in ms. Inside a `KpiCardGrid` it defaults to the card's position times the grid's `stagger`. |
|
|
105
|
+
| `format` | `((value: number) => KpiDisplayValue)` | `formatCount` | Turns the figure (including every in-between value while counting) into what the card shows. Return `{ value, unit }` to show a smaller unit, e.g. `formatDuration`. |
|
|
106
|
+
| `icon` | `ReactNode` | | Icon shown in the tile at the top right, e.g. `<PhoneIcon />`. Sized to 17px. |
|
|
107
|
+
| `loading` | `boolean` | `false` | The figure is not known yet: it holds and pulses, and the trend and tag are hidden. |
|
|
108
|
+
| `showSparkline` | `boolean` | `true` | Shows the `sparkline`. When hidden, the space reserved for it at the bottom goes too. |
|
|
109
|
+
| `showTag` | `boolean` | `true` | Shows `tag`, the pill in the footer (e.g. a share like "42.1%"). The caption stays. |
|
|
110
|
+
| `showTrend` | `boolean` | `true` | Shows `trend`, the change percentage beside the figure. Set `false` to hide it without removing the data. |
|
|
111
|
+
| `sparkline` | `number[]` | | Values for a faint line along the bottom of the card, oldest first. Two or more points. |
|
|
112
|
+
| `tag` | `ReactNode` | | Small pill in the footer, e.g. a share: `"42.1%"`. |
|
|
113
|
+
| `tagTone` | `"brand" \| "success" \| "warning" \| "error" \| "info"` | `brand` | Color of the `tag` pill. |
|
|
114
|
+
| `trend` | `KpiTrend` | | Change against a previous period, shown beside the figure with an arrow. |
|
|
115
|
+
| `variant` | `"glass" \| "solid" \| "elevated" \| "tinted" \| "outlined" \| "accent"` | `glass` | The card's look: `glass`, `solid`, `elevated`, `tinted`, `outlined` or `accent`. |
|
|
116
|
+
<!-- props:end -->
|
|
117
|
+
|
|
118
|
+
### KpiCardGrid
|
|
119
|
+
|
|
120
|
+
<!-- props:start KpiCardGrid -->
|
|
121
|
+
| Prop | Type | Default | Description |
|
|
122
|
+
| --- | --- | --- | --- |
|
|
123
|
+
| `children` | `ReactNode` | | The cards. |
|
|
124
|
+
| `columns` | `number \| Partial<Record<"xs" \| "sm" \| "md" \| "lg" \| "xl", number>>` | `{ xs: 1, sm: 2, xl: 4 }` | Cards per row, at each breakpoint. |
|
|
125
|
+
| `stagger` | `number` | `60` | Delay between one card sliding in and the next, in ms. 0 brings them in together. |
|
|
126
|
+
<!-- props:end -->
|
|
127
|
+
|
|
128
|
+
### KpiSplitBar
|
|
129
|
+
|
|
130
|
+
<!-- props:start KpiSplitBar -->
|
|
131
|
+
| Prop | Type | Default | Description |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| `segments` *(required)* | `KpiSplitBarSegment[]` | | The parts of the whole, in order. |
|
|
134
|
+
| `format` | `((value: number) => string)` | `formatCount` | Formats each segment's value in the legend. |
|
|
135
|
+
<!-- props:end -->
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { BoxProps } from '@mui/material/Box';
|
|
4
|
+
|
|
5
|
+
/** What a card shows for its figure: a string, or a figure with a smaller unit after it ("25h" + "57m"). */
|
|
6
|
+
type KpiDisplayValue = string | {
|
|
7
|
+
value: string;
|
|
8
|
+
unit?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* - `glass`: frosted, translucent card.
|
|
12
|
+
* - `solid`: filled with `color`, white text, for the headline figure.
|
|
13
|
+
* - `elevated`: plain surface with a shadow; lifts on hover.
|
|
14
|
+
* - `tinted`: background washed with `color`; deepens on hover.
|
|
15
|
+
* - `outlined`: border only; the border takes `color` on hover.
|
|
16
|
+
* - `accent`: plain surface with a `color` line along the top that draws in.
|
|
17
|
+
*/
|
|
18
|
+
type KpiCardVariant = 'glass' | 'solid' | 'elevated' | 'tinted' | 'outlined' | 'accent';
|
|
19
|
+
type KpiTone = 'brand' | 'success' | 'warning' | 'error' | 'info';
|
|
20
|
+
interface KpiTrend {
|
|
21
|
+
/** The change itself, e.g. `"12%"`. */
|
|
22
|
+
value: ReactNode;
|
|
23
|
+
/** Which way the arrow points. */
|
|
24
|
+
direction: 'up' | 'down';
|
|
25
|
+
/**
|
|
26
|
+
* Whether the change is good news, which decides the color. Defaults to `up` being
|
|
27
|
+
* positive; set `negative` for figures where up is bad, e.g. churn.
|
|
28
|
+
*/
|
|
29
|
+
sentiment?: 'positive' | 'negative';
|
|
30
|
+
}
|
|
31
|
+
interface KpiCardProps extends Omit<BoxProps, 'children' | 'component'> {
|
|
32
|
+
/** What the figure measures, e.g. "Connected Calls". */
|
|
33
|
+
label: ReactNode;
|
|
34
|
+
/** The figure. Counted up to on mount, and from the old figure to the new one when it changes. */
|
|
35
|
+
value: number;
|
|
36
|
+
/**
|
|
37
|
+
* Turns the figure (including every in-between value while counting) into what the card
|
|
38
|
+
* shows. Return `{ value, unit }` to show a smaller unit, e.g. `formatDuration`.
|
|
39
|
+
* @default formatCount
|
|
40
|
+
*/
|
|
41
|
+
format?: (value: number) => KpiDisplayValue;
|
|
42
|
+
/**
|
|
43
|
+
* Decimal places the count-up keeps, for fractional figures like rates.
|
|
44
|
+
* @default 0
|
|
45
|
+
*/
|
|
46
|
+
decimals?: number;
|
|
47
|
+
/** Icon shown in the tile at the top right, e.g. `<PhoneIcon />`. Sized to 17px. */
|
|
48
|
+
icon?: ReactNode;
|
|
49
|
+
/** Change against a previous period, shown beside the figure with an arrow. */
|
|
50
|
+
trend?: KpiTrend;
|
|
51
|
+
/** Small pill in the footer, e.g. a share: `"42.1%"`. */
|
|
52
|
+
tag?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Color of the `tag` pill.
|
|
55
|
+
* @default 'brand'
|
|
56
|
+
*/
|
|
57
|
+
tagTone?: KpiTone;
|
|
58
|
+
/** Muted text after the tag, e.g. "of total calls". */
|
|
59
|
+
caption?: ReactNode;
|
|
60
|
+
/** Values for a faint line along the bottom of the card, oldest first. Two or more points. */
|
|
61
|
+
sparkline?: number[];
|
|
62
|
+
/**
|
|
63
|
+
* Shows `trend`, the change percentage beside the figure. Set `false` to hide it without
|
|
64
|
+
* removing the data.
|
|
65
|
+
* @default true
|
|
66
|
+
*/
|
|
67
|
+
showTrend?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Shows `tag`, the pill in the footer (e.g. a share like "42.1%"). The caption stays.
|
|
70
|
+
* @default true
|
|
71
|
+
*/
|
|
72
|
+
showTag?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Shows the `sparkline`. When hidden, the space reserved for it at the bottom goes too.
|
|
75
|
+
* @default true
|
|
76
|
+
*/
|
|
77
|
+
showSparkline?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The card's look: `glass`, `solid`, `elevated`, `tinted`, `outlined` or `accent`.
|
|
80
|
+
* @default 'glass'
|
|
81
|
+
*/
|
|
82
|
+
variant?: KpiCardVariant;
|
|
83
|
+
/**
|
|
84
|
+
* The card's color: the figure, icon, sparkline, and the fill, tint or line of the
|
|
85
|
+
* `solid`, `tinted` and `accent` variants.
|
|
86
|
+
* @default 'brand'
|
|
87
|
+
*/
|
|
88
|
+
color?: KpiTone;
|
|
89
|
+
/** The figure is not known yet: it holds and pulses, and the trend and tag are hidden. */
|
|
90
|
+
loading?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Animates the card: it slides in, the figure counts up and the sparkline draws itself.
|
|
93
|
+
* Always off for viewers who ask for reduced motion.
|
|
94
|
+
* @default true
|
|
95
|
+
*/
|
|
96
|
+
animate?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* How long the count-up takes, in ms.
|
|
99
|
+
* @default 900
|
|
100
|
+
*/
|
|
101
|
+
countUpDuration?: number;
|
|
102
|
+
/** Delay before the card slides in, in ms. Inside a `KpiCardGrid` it defaults to the card's position times the grid's `stagger`. */
|
|
103
|
+
enterDelay?: number;
|
|
104
|
+
/** Extra content under the figure, e.g. a `KpiSplitBar`. Replaces the footer unless `tag` or `caption` is set. */
|
|
105
|
+
children?: ReactNode;
|
|
106
|
+
}
|
|
107
|
+
interface KpiCardGridProps extends BoxProps {
|
|
108
|
+
/**
|
|
109
|
+
* Cards per row, at each breakpoint.
|
|
110
|
+
* @default { xs: 1, sm: 2, xl: 4 }
|
|
111
|
+
*/
|
|
112
|
+
columns?: number | Partial<Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', number>>;
|
|
113
|
+
/**
|
|
114
|
+
* Delay between one card sliding in and the next, in ms. 0 brings them in together.
|
|
115
|
+
* @default 60
|
|
116
|
+
*/
|
|
117
|
+
stagger?: number;
|
|
118
|
+
/** The cards. */
|
|
119
|
+
children?: ReactNode;
|
|
120
|
+
}
|
|
121
|
+
interface KpiSplitBarSegment {
|
|
122
|
+
/** Legend label, e.g. "AI Calls". */
|
|
123
|
+
label: ReactNode;
|
|
124
|
+
/** The segment's size. Widths are shares of the segments' total. */
|
|
125
|
+
value: number;
|
|
126
|
+
/** Bar and legend dot color. Defaults to the card's accent, fainter for each later segment. */
|
|
127
|
+
color?: string;
|
|
128
|
+
}
|
|
129
|
+
interface KpiSplitBarProps extends BoxProps {
|
|
130
|
+
/** The parts of the whole, in order. */
|
|
131
|
+
segments: KpiSplitBarSegment[];
|
|
132
|
+
/**
|
|
133
|
+
* Formats each segment's value in the legend.
|
|
134
|
+
* @default formatCount
|
|
135
|
+
*/
|
|
136
|
+
format?: (value: number) => string;
|
|
137
|
+
}
|
|
138
|
+
interface KpiSparklineProps {
|
|
139
|
+
/** Values to draw, oldest first. */
|
|
140
|
+
data: number[];
|
|
141
|
+
/**
|
|
142
|
+
* Line color; the area under it uses the same color, fainter.
|
|
143
|
+
* @default 'currentColor'
|
|
144
|
+
*/
|
|
145
|
+
color?: string;
|
|
146
|
+
/**
|
|
147
|
+
* Draws the line in from left to right on mount.
|
|
148
|
+
* @default true
|
|
149
|
+
*/
|
|
150
|
+
animate?: boolean;
|
|
151
|
+
/** Class name for the `<svg>`. */
|
|
152
|
+
className?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A dashboard figure: label and icon, a big number that counts up, an optional trend,
|
|
157
|
+
* a footer pill with a caption, and a faint sparkline. The cards slide in one after
|
|
158
|
+
* another inside a `KpiCardGrid`.
|
|
159
|
+
*/
|
|
160
|
+
declare const KpiCard: react.ForwardRefExoticComponent<Omit<KpiCardProps, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Lays `KpiCard`s out in a responsive grid and brings them in one after another,
|
|
164
|
+
* `stagger` ms apart, so a row of figures reads as a sweep rather than a flash.
|
|
165
|
+
*/
|
|
166
|
+
declare const KpiCardGrid: react.ForwardRefExoticComponent<Omit<KpiCardGridProps, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A single bar split into the parts of a whole, with a legend of counts underneath,
|
|
170
|
+
* e.g. AI against manual calls. The segments grow in from zero. Inside a `KpiCard` it
|
|
171
|
+
* takes the card's colors and waits for its `loading` to finish.
|
|
172
|
+
*/
|
|
173
|
+
declare const KpiSplitBar: react.ForwardRefExoticComponent<Omit<KpiSplitBarProps, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A small decorative trend line with a faint fill. Stretches to its box; `KpiCard` lays it
|
|
177
|
+
* along its bottom edge. Hidden from screen readers.
|
|
178
|
+
*/
|
|
179
|
+
declare function KpiSparkline({ data, color, animate, className }: KpiSparklineProps): react.JSX.Element | null;
|
|
180
|
+
|
|
181
|
+
interface UseCountUpOptions {
|
|
182
|
+
/**
|
|
183
|
+
* Off while the figure is unknown, so nothing counts toward a guess. The figure is
|
|
184
|
+
* shown as is.
|
|
185
|
+
* @default true
|
|
186
|
+
*/
|
|
187
|
+
enabled?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Length of the count, in ms.
|
|
190
|
+
* @default 900
|
|
191
|
+
*/
|
|
192
|
+
duration?: number;
|
|
193
|
+
/**
|
|
194
|
+
* Decimal places to round to on every frame.
|
|
195
|
+
* @default 0
|
|
196
|
+
*/
|
|
197
|
+
decimals?: number;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Counts from the figure on screen to `value`: from 0 on mount, then from the old figure
|
|
201
|
+
* to the new one, so changing a filter nudges figures instead of restarting them at 0.
|
|
202
|
+
* Every frame is rounded to `decimals`, and the last frame is `value` exactly.
|
|
203
|
+
* Returns `value` straight away for viewers who ask for reduced motion.
|
|
204
|
+
*/
|
|
205
|
+
declare function useCountUp(value: number, { enabled, duration, decimals }?: UseCountUpOptions): number;
|
|
206
|
+
|
|
207
|
+
/** A whole number with thousands separators: 93420 → "93,420". */
|
|
208
|
+
declare function formatCount(value: number): string;
|
|
209
|
+
/**
|
|
210
|
+
* Seconds as the two largest units, the second shown smaller by `KpiCard`:
|
|
211
|
+
* 93420 → "25h" + "57m", 292 → "4m" + "52s", 40 → "40s".
|
|
212
|
+
*/
|
|
213
|
+
declare function formatDuration(seconds: number): Exclude<KpiDisplayValue, string>;
|
|
214
|
+
/** `part` as a share of `whole`: (42, 100) → "42.0%". `undefined` when either is zero, so there's no "0%" pill. */
|
|
215
|
+
declare function formatShare(part: number, whole: number, decimals?: number): string | undefined;
|
|
216
|
+
|
|
217
|
+
export { KpiCard, KpiCardGrid, type KpiCardGridProps, type KpiCardProps, type KpiCardVariant, type KpiDisplayValue, KpiSparkline, type KpiSparklineProps, KpiSplitBar, type KpiSplitBarProps, type KpiSplitBarSegment, type KpiTone, type KpiTrend, type UseCountUpOptions, formatCount, formatDuration, formatShare, useCountUp };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/KpiCard.tsx
|
|
4
|
+
import { forwardRef, useContext, useMemo } from "react";
|
|
5
|
+
import Box from "@mui/material/Box";
|
|
6
|
+
import { styled as styled2 } from "@mui/material/styles";
|
|
7
|
+
import { tokens } from "@dax-ui/tokens";
|
|
8
|
+
|
|
9
|
+
// src/KpiCard.constants.ts
|
|
10
|
+
import { createContext } from "react";
|
|
11
|
+
import { keyframes } from "@mui/material/styles";
|
|
12
|
+
var SPARKLINE_HEIGHT = 64;
|
|
13
|
+
var ENTER_DURATION = 500;
|
|
14
|
+
var DEFAULT_STAGGER = 60;
|
|
15
|
+
var EASE_OUT = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
16
|
+
var cssVar = {
|
|
17
|
+
/** Figure, icon, sparkline and the first split-bar segment. */
|
|
18
|
+
accent: "var(--dax-kpi-accent)",
|
|
19
|
+
/** Unfilled part of a split bar. */
|
|
20
|
+
track: "var(--dax-kpi-track)",
|
|
21
|
+
/** Label, caption and legend text. */
|
|
22
|
+
muted: "var(--dax-kpi-muted)"
|
|
23
|
+
};
|
|
24
|
+
var enter = keyframes`
|
|
25
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
26
|
+
`;
|
|
27
|
+
var draw = keyframes`
|
|
28
|
+
from { stroke-dashoffset: 1; }
|
|
29
|
+
to { stroke-dashoffset: 0; }
|
|
30
|
+
`;
|
|
31
|
+
var fadeIn = keyframes`
|
|
32
|
+
from { opacity: 0; }
|
|
33
|
+
`;
|
|
34
|
+
var growX = keyframes`
|
|
35
|
+
from { transform: scaleX(0); }
|
|
36
|
+
`;
|
|
37
|
+
var pulse = keyframes`
|
|
38
|
+
50% { opacity: 0.45; }
|
|
39
|
+
`;
|
|
40
|
+
var KpiCardContext = createContext(null);
|
|
41
|
+
var KpiGridSlotContext = createContext(null);
|
|
42
|
+
|
|
43
|
+
// src/KpiSparkline.tsx
|
|
44
|
+
import { styled } from "@mui/material/styles";
|
|
45
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
46
|
+
var WIDTH = 200;
|
|
47
|
+
var HEIGHT = 60;
|
|
48
|
+
var Svg = styled("svg", { shouldForwardProp: (p) => p !== "animate" })(({ animate }) => ({
|
|
49
|
+
display: "block",
|
|
50
|
+
pointerEvents: "none",
|
|
51
|
+
overflow: "visible",
|
|
52
|
+
...animate && {
|
|
53
|
+
"& .dax-kpi-spark-line": { strokeDasharray: 1, animation: `${draw} 1.1s ${EASE_OUT} backwards` },
|
|
54
|
+
"& .dax-kpi-spark-area": { animation: `${fadeIn} 0.9s 0.35s ease-out backwards` },
|
|
55
|
+
// A line drawing itself is decoration; reduced motion gets the finished shape.
|
|
56
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
57
|
+
"& .dax-kpi-spark-line, & .dax-kpi-spark-area": { animation: "none", strokeDasharray: "none" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
function paths(data) {
|
|
62
|
+
const max = Math.max(...data);
|
|
63
|
+
const min = Math.min(...data);
|
|
64
|
+
const span = max - min || 1;
|
|
65
|
+
const steps = data.length - 1 || 1;
|
|
66
|
+
const line = data.map((value, index) => {
|
|
67
|
+
const x = index / steps * WIDTH;
|
|
68
|
+
const y = 58 - (value - min) / span * 26;
|
|
69
|
+
return `${index ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`;
|
|
70
|
+
}).join(" ");
|
|
71
|
+
return { line, area: `${line} L${WIDTH} ${HEIGHT} L0 ${HEIGHT} Z` };
|
|
72
|
+
}
|
|
73
|
+
function KpiSparkline({ data, color = "currentColor", animate = true, className }) {
|
|
74
|
+
if (data.length < 2) return null;
|
|
75
|
+
const { line, area } = paths(data);
|
|
76
|
+
return /* @__PURE__ */ jsxs(
|
|
77
|
+
Svg,
|
|
78
|
+
{
|
|
79
|
+
viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
|
|
80
|
+
preserveAspectRatio: "none",
|
|
81
|
+
"aria-hidden": true,
|
|
82
|
+
focusable: "false",
|
|
83
|
+
animate,
|
|
84
|
+
className,
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ jsx("path", { d: area, pathLength: 1, className: "dax-kpi-spark-area", fill: color, fillOpacity: 0.08 }),
|
|
87
|
+
/* @__PURE__ */ jsx(
|
|
88
|
+
"path",
|
|
89
|
+
{
|
|
90
|
+
d: line,
|
|
91
|
+
pathLength: 1,
|
|
92
|
+
className: "dax-kpi-spark-line",
|
|
93
|
+
fill: "none",
|
|
94
|
+
stroke: color,
|
|
95
|
+
strokeOpacity: 0.35,
|
|
96
|
+
strokeWidth: 1.6,
|
|
97
|
+
strokeLinejoin: "round",
|
|
98
|
+
strokeLinecap: "round",
|
|
99
|
+
vectorEffect: "non-scaling-stroke"
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/format.ts
|
|
108
|
+
function formatCount(value) {
|
|
109
|
+
return Math.round(value).toLocaleString("en-US");
|
|
110
|
+
}
|
|
111
|
+
function formatDuration(seconds) {
|
|
112
|
+
const total = Math.max(0, Math.round(seconds));
|
|
113
|
+
const hours = Math.floor(total / 3600);
|
|
114
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
115
|
+
if (hours > 0) return { value: `${formatCount(hours)}h`, unit: `${minutes}m` };
|
|
116
|
+
if (minutes > 0) return { value: `${minutes}m`, unit: `${total % 60}s` };
|
|
117
|
+
return { value: `${total}s` };
|
|
118
|
+
}
|
|
119
|
+
function formatShare(part, whole, decimals = 1) {
|
|
120
|
+
if (!whole || !part) return void 0;
|
|
121
|
+
return `${(part / whole * 100).toFixed(decimals)}%`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/useCountUp.ts
|
|
125
|
+
import { useEffect, useRef, useState } from "react";
|
|
126
|
+
var easeOutExpo = (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
|
|
127
|
+
var roundTo = (n, decimals) => Number(n.toFixed(decimals));
|
|
128
|
+
var prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
129
|
+
function useCountUp(value, { enabled = true, duration = 900, decimals = 0 } = {}) {
|
|
130
|
+
const [shown, setShown] = useState(() => enabled && !prefersReducedMotion() ? 0 : roundTo(value, decimals));
|
|
131
|
+
const fromRef = useRef(0);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const target = Number.isFinite(value) ? value : 0;
|
|
134
|
+
if (!enabled || duration <= 0 || prefersReducedMotion()) {
|
|
135
|
+
fromRef.current = target;
|
|
136
|
+
setShown(roundTo(target, decimals));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const from = fromRef.current;
|
|
140
|
+
if (from === target) {
|
|
141
|
+
setShown(roundTo(target, decimals));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const start = performance.now();
|
|
145
|
+
let frame = requestAnimationFrame(function tick() {
|
|
146
|
+
const progress = Math.min(1, (performance.now() - start) / duration);
|
|
147
|
+
const next = progress === 1 ? target : from + (target - from) * easeOutExpo(progress);
|
|
148
|
+
fromRef.current = next;
|
|
149
|
+
setShown(roundTo(next, decimals));
|
|
150
|
+
if (progress < 1) frame = requestAnimationFrame(tick);
|
|
151
|
+
});
|
|
152
|
+
return () => cancelAnimationFrame(frame);
|
|
153
|
+
}, [value, enabled, duration, decimals]);
|
|
154
|
+
return shown;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/visuallyHidden.ts
|
|
158
|
+
var visuallyHidden = {
|
|
159
|
+
position: "absolute",
|
|
160
|
+
width: 1,
|
|
161
|
+
height: 1,
|
|
162
|
+
margin: -1,
|
|
163
|
+
padding: 0,
|
|
164
|
+
overflow: "hidden",
|
|
165
|
+
clip: "rect(0 0 0 0)",
|
|
166
|
+
whiteSpace: "nowrap",
|
|
167
|
+
border: 0
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/KpiCard.tsx
|
|
171
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
172
|
+
var { brand } = tokens.color;
|
|
173
|
+
var palette = (theme) => (theme.vars ?? theme).palette;
|
|
174
|
+
var isSolid = (variant) => variant === "solid";
|
|
175
|
+
var toneColor = (theme, tone) => tone === "brand" ? palette(theme).primary.main : palette(theme)[tone].main;
|
|
176
|
+
var surface = (theme) => ({
|
|
177
|
+
color: palette(theme).text.primary,
|
|
178
|
+
backgroundColor: palette(theme).background.paper
|
|
179
|
+
});
|
|
180
|
+
function variantStyles(theme, variant, tone) {
|
|
181
|
+
const main = toneColor(theme, tone);
|
|
182
|
+
const ring = `inset 0 0 0 1px ${palette(theme).divider}`;
|
|
183
|
+
switch (variant) {
|
|
184
|
+
case "solid":
|
|
185
|
+
return {
|
|
186
|
+
"--dax-kpi-accent": "#ffffff",
|
|
187
|
+
"--dax-kpi-track": "rgba(255, 255, 255, 0.15)",
|
|
188
|
+
"--dax-kpi-muted": "rgba(255, 255, 255, 0.85)",
|
|
189
|
+
color: "#ffffff",
|
|
190
|
+
// The same fill in light and dark mode.
|
|
191
|
+
backgroundImage: tone === "brand" ? `linear-gradient(150deg, ${brand[500]} 0%, ${brand[700]} 100%)` : `linear-gradient(150deg, ${main} 0%, ${palette(theme)[tone].dark} 100%)`,
|
|
192
|
+
boxShadow: `inset 0 0 0 1px rgba(255, 255, 255, 0.3), 0 24px 48px -22px ${theme.alpha(main, 0.75)}`
|
|
193
|
+
};
|
|
194
|
+
case "elevated":
|
|
195
|
+
return {
|
|
196
|
+
...surface(theme),
|
|
197
|
+
boxShadow: "0 1px 2px rgba(15, 23, 42, 0.06), 0 12px 32px -12px rgba(15, 23, 42, 0.22)",
|
|
198
|
+
"&:hover": {
|
|
199
|
+
transform: "translateY(-3px)",
|
|
200
|
+
boxShadow: "0 2px 4px rgba(15, 23, 42, 0.06), 0 24px 48px -16px rgba(15, 23, 42, 0.32)"
|
|
201
|
+
},
|
|
202
|
+
...theme.applyStyles("dark", {
|
|
203
|
+
boxShadow: `${ring}, 0 16px 40px -16px rgba(0, 0, 0, 0.7)`,
|
|
204
|
+
"&:hover": { transform: "translateY(-3px)", boxShadow: `${ring}, 0 28px 56px -18px rgba(0, 0, 0, 0.8)` }
|
|
205
|
+
})
|
|
206
|
+
};
|
|
207
|
+
case "tinted":
|
|
208
|
+
return {
|
|
209
|
+
color: palette(theme).text.primary,
|
|
210
|
+
backgroundColor: theme.alpha(main, 0.08),
|
|
211
|
+
boxShadow: `inset 0 0 0 1px ${theme.alpha(main, 0.16)}`,
|
|
212
|
+
"&:hover": { backgroundColor: theme.alpha(main, 0.15) }
|
|
213
|
+
};
|
|
214
|
+
case "outlined":
|
|
215
|
+
return {
|
|
216
|
+
...surface(theme),
|
|
217
|
+
boxShadow: ring,
|
|
218
|
+
"&:hover": { boxShadow: `inset 0 0 0 1.5px ${main}` }
|
|
219
|
+
};
|
|
220
|
+
case "accent":
|
|
221
|
+
return {
|
|
222
|
+
...surface(theme),
|
|
223
|
+
boxShadow: `${ring}, 0 8px 24px -16px rgba(15, 23, 42, 0.25)`,
|
|
224
|
+
// The line along the top grows in from the left, then thickens on hover.
|
|
225
|
+
"&::before": {
|
|
226
|
+
content: '""',
|
|
227
|
+
position: "absolute",
|
|
228
|
+
insetInline: 0,
|
|
229
|
+
top: 0,
|
|
230
|
+
height: 4,
|
|
231
|
+
backgroundColor: main,
|
|
232
|
+
transformOrigin: "left",
|
|
233
|
+
transition: `height 250ms ${EASE_OUT}`,
|
|
234
|
+
animation: `${growX} 700ms 150ms ${EASE_OUT} backwards`,
|
|
235
|
+
"@media (prefers-reduced-motion: reduce)": { animation: "none" }
|
|
236
|
+
},
|
|
237
|
+
"&:hover::before": { height: 6 }
|
|
238
|
+
};
|
|
239
|
+
case "glass":
|
|
240
|
+
default:
|
|
241
|
+
return {
|
|
242
|
+
color: palette(theme).text.primary,
|
|
243
|
+
// Frosted: whatever sits behind the cards shows through, blurred.
|
|
244
|
+
backgroundColor: theme.alpha(palette(theme).background.paper, 0.6),
|
|
245
|
+
backdropFilter: "blur(16px) saturate(1.4)",
|
|
246
|
+
WebkitBackdropFilter: "blur(16px) saturate(1.4)",
|
|
247
|
+
boxShadow: `${ring}, inset 0 1px 0 rgba(255, 255, 255, 0.6), 0 20px 40px -26px rgba(15, 23, 42, 0.35)`,
|
|
248
|
+
...theme.applyStyles("dark", { boxShadow: `${ring}, 0 20px 40px -26px rgba(0, 0, 0, 0.6)` })
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
var Root = styled2(Box, {
|
|
253
|
+
shouldForwardProp: (p) => p !== "variant" && p !== "tone" && p !== "animate" && p !== "hasSparkline"
|
|
254
|
+
})(
|
|
255
|
+
({ theme, variant, tone, animate, hasSparkline }) => ({
|
|
256
|
+
position: "relative",
|
|
257
|
+
isolation: "isolate",
|
|
258
|
+
overflow: "hidden",
|
|
259
|
+
display: "flex",
|
|
260
|
+
flexDirection: "column",
|
|
261
|
+
justifyContent: "space-between",
|
|
262
|
+
gap: 16,
|
|
263
|
+
minWidth: 0,
|
|
264
|
+
padding: 20,
|
|
265
|
+
paddingBottom: hasSparkline ? 36 : 20,
|
|
266
|
+
borderRadius: 20,
|
|
267
|
+
transition: `transform 250ms ${EASE_OUT}, box-shadow 250ms ${EASE_OUT}, background-color 250ms ${EASE_OUT}`,
|
|
268
|
+
// `solid` sets its own; skipping these keeps the dark-mode rule from overriding its white.
|
|
269
|
+
...!isSolid(variant) && {
|
|
270
|
+
"--dax-kpi-accent": tone === "brand" ? palette(theme).primary.dark : toneColor(theme, tone),
|
|
271
|
+
"--dax-kpi-track": theme.alpha(toneColor(theme, tone), 0.14),
|
|
272
|
+
"--dax-kpi-muted": palette(theme).text.secondary,
|
|
273
|
+
...theme.applyStyles("dark", { "--dax-kpi-accent": toneColor(theme, tone) })
|
|
274
|
+
},
|
|
275
|
+
...animate && { animation: `${enter} ${ENTER_DURATION}ms ${EASE_OUT} backwards` },
|
|
276
|
+
...variantStyles(theme, variant, tone),
|
|
277
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
278
|
+
animation: "none",
|
|
279
|
+
transition: "none",
|
|
280
|
+
"&:hover": { transform: "none" }
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
var Row = styled2("div")({ position: "relative", display: "flex", alignItems: "center", gap: 12 });
|
|
285
|
+
var Label = styled2("span")({ fontSize: 13.5, fontWeight: 500, color: cssVar.muted });
|
|
286
|
+
var IconTile = styled2("span", { shouldForwardProp: (p) => p !== "variant" })(
|
|
287
|
+
({ theme, variant }) => ({
|
|
288
|
+
display: "flex",
|
|
289
|
+
flexShrink: 0,
|
|
290
|
+
alignItems: "center",
|
|
291
|
+
justifyContent: "center",
|
|
292
|
+
width: 34,
|
|
293
|
+
height: 34,
|
|
294
|
+
marginLeft: "auto",
|
|
295
|
+
borderRadius: 11,
|
|
296
|
+
color: cssVar.accent,
|
|
297
|
+
backgroundColor: isSolid(variant) ? "rgba(255, 255, 255, 0.2)" : `color-mix(in srgb, ${cssVar.accent} 10%, transparent)`,
|
|
298
|
+
"& svg": { width: 17, height: 17 }
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
var Figure = styled2("span", { shouldForwardProp: (p) => p !== "loading" })(({ loading }) => ({
|
|
302
|
+
fontSize: 40,
|
|
303
|
+
fontWeight: 700,
|
|
304
|
+
lineHeight: 1,
|
|
305
|
+
letterSpacing: "-0.03em",
|
|
306
|
+
fontVariantNumeric: "tabular-nums",
|
|
307
|
+
color: cssVar.accent,
|
|
308
|
+
transition: "opacity 300ms",
|
|
309
|
+
...loading && { opacity: 0.45, animation: `${pulse} 1.6s ease-in-out infinite` }
|
|
310
|
+
}));
|
|
311
|
+
var Unit = styled2("span")({ marginLeft: 4, fontSize: 20, letterSpacing: "-0.02em", opacity: 0.7 });
|
|
312
|
+
var fadeWhileLoading = (loading) => ({ transition: "opacity 500ms", opacity: loading ? 0 : 1 });
|
|
313
|
+
var Trend = styled2("span", { shouldForwardProp: (p) => p !== "tone" && p !== "loading" })(({ theme, tone, loading }) => ({
|
|
314
|
+
display: "inline-flex",
|
|
315
|
+
alignItems: "center",
|
|
316
|
+
gap: 2,
|
|
317
|
+
fontSize: 12,
|
|
318
|
+
fontWeight: 600,
|
|
319
|
+
color: tone === "onBrand" ? "#ffffff" : palette(theme)[tone].main,
|
|
320
|
+
"& svg": { width: 12, height: 12 },
|
|
321
|
+
...fadeWhileLoading(loading)
|
|
322
|
+
}));
|
|
323
|
+
var Tag = styled2("span", { shouldForwardProp: (p) => p !== "tone" && p !== "variant" && p !== "loading" })(({ theme, tone, variant, loading }) => {
|
|
324
|
+
const color = toneColor(theme, tone);
|
|
325
|
+
return {
|
|
326
|
+
padding: "2px 8px",
|
|
327
|
+
borderRadius: 999,
|
|
328
|
+
fontSize: 11,
|
|
329
|
+
fontWeight: 600,
|
|
330
|
+
whiteSpace: "nowrap",
|
|
331
|
+
...isSolid(variant) ? { color: "#ffffff", backgroundColor: "rgba(255, 255, 255, 0.2)" } : { color: tone === "brand" ? cssVar.accent : color, backgroundColor: theme.alpha(color, 0.1) },
|
|
332
|
+
...fadeWhileLoading(loading)
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
var Sparkline = styled2(KpiSparkline)({
|
|
336
|
+
position: "absolute",
|
|
337
|
+
insetInline: 0,
|
|
338
|
+
bottom: 0,
|
|
339
|
+
width: "100%",
|
|
340
|
+
height: SPARKLINE_HEIGHT
|
|
341
|
+
});
|
|
342
|
+
function Arrow({ direction }) {
|
|
343
|
+
return /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: direction === "up" ? /* @__PURE__ */ jsx2("path", { d: "M7 17 17 7M7 7h10v10" }) : /* @__PURE__ */ jsx2("path", { d: "M7 7l10 10M17 7v10H7" }) });
|
|
344
|
+
}
|
|
345
|
+
var asText = (display) => typeof display === "string" ? display : [display.value, display.unit].filter(Boolean).join(" ");
|
|
346
|
+
var KpiCard = forwardRef(function KpiCard2({
|
|
347
|
+
label,
|
|
348
|
+
value,
|
|
349
|
+
format = formatCount,
|
|
350
|
+
decimals = 0,
|
|
351
|
+
icon,
|
|
352
|
+
trend,
|
|
353
|
+
tag,
|
|
354
|
+
tagTone = "brand",
|
|
355
|
+
caption,
|
|
356
|
+
sparkline,
|
|
357
|
+
showTrend = true,
|
|
358
|
+
showTag = true,
|
|
359
|
+
showSparkline = true,
|
|
360
|
+
variant = "glass",
|
|
361
|
+
color = "brand",
|
|
362
|
+
loading = false,
|
|
363
|
+
animate = true,
|
|
364
|
+
countUpDuration = 900,
|
|
365
|
+
enterDelay,
|
|
366
|
+
children,
|
|
367
|
+
style,
|
|
368
|
+
...rest
|
|
369
|
+
}, ref) {
|
|
370
|
+
const slot = useContext(KpiGridSlotContext);
|
|
371
|
+
const delay = enterDelay ?? (slot ? slot.index * slot.stagger : 0);
|
|
372
|
+
const shown = useCountUp(value, { enabled: animate && !loading, duration: countUpDuration, decimals });
|
|
373
|
+
const display = format(shown);
|
|
374
|
+
const figure = typeof display === "string" ? { value: display } : display;
|
|
375
|
+
const hasSparkline = showSparkline && Boolean(sparkline && sparkline.length > 1);
|
|
376
|
+
const visibleTag = showTag ? tag : void 0;
|
|
377
|
+
const showFooter = Boolean(visibleTag || caption) || !children;
|
|
378
|
+
const trendTone = isSolid(variant) ? "onBrand" : (trend?.sentiment ?? (trend?.direction === "up" ? "positive" : "negative")) === "positive" ? "success" : "error";
|
|
379
|
+
const context = useMemo(
|
|
380
|
+
() => ({ variant, loading, animate, countUpDuration }),
|
|
381
|
+
[variant, loading, animate, countUpDuration]
|
|
382
|
+
);
|
|
383
|
+
return /* @__PURE__ */ jsx2(KpiCardContext.Provider, { value: context, children: /* @__PURE__ */ jsxs2(
|
|
384
|
+
Root,
|
|
385
|
+
{
|
|
386
|
+
ref,
|
|
387
|
+
variant,
|
|
388
|
+
tone: color,
|
|
389
|
+
animate,
|
|
390
|
+
hasSparkline,
|
|
391
|
+
"aria-busy": loading || void 0,
|
|
392
|
+
style: { animationDelay: delay ? `${delay}ms` : void 0, ...style },
|
|
393
|
+
...rest,
|
|
394
|
+
children: [
|
|
395
|
+
hasSparkline && /* @__PURE__ */ jsx2(Sparkline, { data: sparkline, color: cssVar.accent, animate }),
|
|
396
|
+
/* @__PURE__ */ jsxs2(Row, { children: [
|
|
397
|
+
/* @__PURE__ */ jsx2(Label, { children: label }),
|
|
398
|
+
icon && /* @__PURE__ */ jsx2(IconTile, { variant, children: icon })
|
|
399
|
+
] }),
|
|
400
|
+
/* @__PURE__ */ jsxs2(Row, { sx: { flexWrap: "wrap", alignItems: "baseline", columnGap: "10px", rowGap: "4px" }, children: [
|
|
401
|
+
/* @__PURE__ */ jsxs2(Figure, { loading, "aria-hidden": true, children: [
|
|
402
|
+
figure.value,
|
|
403
|
+
figure.unit && /* @__PURE__ */ jsx2(Unit, { children: figure.unit })
|
|
404
|
+
] }),
|
|
405
|
+
/* @__PURE__ */ jsx2("span", { style: visuallyHidden, children: loading ? "Loading" : asText(format(value)) }),
|
|
406
|
+
trend && showTrend && /* @__PURE__ */ jsxs2(Trend, { tone: trendTone, loading, children: [
|
|
407
|
+
/* @__PURE__ */ jsx2(Arrow, { direction: trend.direction }),
|
|
408
|
+
/* @__PURE__ */ jsx2("span", { style: visuallyHidden, children: trend.direction === "up" ? "Up " : "Down " }),
|
|
409
|
+
trend.value
|
|
410
|
+
] })
|
|
411
|
+
] }),
|
|
412
|
+
children && /* @__PURE__ */ jsx2(Box, { sx: { position: "relative" }, children }),
|
|
413
|
+
showFooter && /* @__PURE__ */ jsxs2(Row, { sx: { minHeight: 22, gap: 1, fontSize: 12, color: cssVar.muted }, children: [
|
|
414
|
+
visibleTag && /* @__PURE__ */ jsx2(Tag, { tone: tagTone, variant, loading, children: visibleTag }),
|
|
415
|
+
caption
|
|
416
|
+
] })
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
) });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// src/KpiCardGrid.tsx
|
|
423
|
+
import { Children, forwardRef as forwardRef2 } from "react";
|
|
424
|
+
import Box2 from "@mui/material/Box";
|
|
425
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
426
|
+
var DEFAULT_COLUMNS = { xs: 1, sm: 2, xl: 4 };
|
|
427
|
+
var KpiCardGrid = forwardRef2(function KpiCardGrid2({ columns = DEFAULT_COLUMNS, stagger = DEFAULT_STAGGER, children, sx, ...rest }, ref) {
|
|
428
|
+
const templateColumns = typeof columns === "number" ? `repeat(${columns}, minmax(0, 1fr))` : Object.fromEntries(Object.entries(columns).map(([bp, n]) => [bp, `repeat(${n}, minmax(0, 1fr))`]));
|
|
429
|
+
return /* @__PURE__ */ jsx3(
|
|
430
|
+
Box2,
|
|
431
|
+
{
|
|
432
|
+
ref,
|
|
433
|
+
sx: [{ display: "grid", gap: 2, gridTemplateColumns: templateColumns }, ...Array.isArray(sx) ? sx : [sx]],
|
|
434
|
+
...rest,
|
|
435
|
+
children: Children.toArray(children).map((child, index) => /* @__PURE__ */ jsx3(KpiGridSlotContext.Provider, { value: { index, stagger }, children: child }, index))
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// src/KpiSplitBar.tsx
|
|
441
|
+
import { forwardRef as forwardRef3, useContext as useContext2 } from "react";
|
|
442
|
+
import Box3 from "@mui/material/Box";
|
|
443
|
+
import { styled as styled3 } from "@mui/material/styles";
|
|
444
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
445
|
+
var SEGMENT_OPACITY = [1, 0.45, 0.25, 0.15];
|
|
446
|
+
var Track = styled3("div")({
|
|
447
|
+
display: "flex",
|
|
448
|
+
gap: 2,
|
|
449
|
+
height: 6,
|
|
450
|
+
overflow: "hidden",
|
|
451
|
+
borderRadius: 999,
|
|
452
|
+
backgroundColor: cssVar.track
|
|
453
|
+
});
|
|
454
|
+
var Legend = styled3("div")({
|
|
455
|
+
display: "flex",
|
|
456
|
+
flexWrap: "wrap",
|
|
457
|
+
justifyContent: "space-between",
|
|
458
|
+
gap: "4px 12px",
|
|
459
|
+
marginTop: 8,
|
|
460
|
+
fontSize: 11,
|
|
461
|
+
color: cssVar.muted,
|
|
462
|
+
fontVariantNumeric: "tabular-nums"
|
|
463
|
+
});
|
|
464
|
+
function LegendValue({ value, format, enabled, duration }) {
|
|
465
|
+
return /* @__PURE__ */ jsx4(Fragment, { children: format(useCountUp(value, { enabled, duration })) });
|
|
466
|
+
}
|
|
467
|
+
var KpiSplitBar = forwardRef3(function KpiSplitBar2({ segments, format = formatCount, ...rest }, ref) {
|
|
468
|
+
const card = useContext2(KpiCardContext);
|
|
469
|
+
const loading = card?.loading ?? false;
|
|
470
|
+
const animate = card?.animate ?? true;
|
|
471
|
+
const duration = card?.countUpDuration ?? 900;
|
|
472
|
+
const total = segments.reduce((sum, s) => sum + Math.max(0, s.value), 0);
|
|
473
|
+
const colorOf = (segment) => segment.color ?? cssVar.accent;
|
|
474
|
+
const opacityOf = (segment, index) => segment.color ? 1 : SEGMENT_OPACITY[index] ?? 0.15;
|
|
475
|
+
return /* @__PURE__ */ jsxs3(Box3, { ref, ...rest, children: [
|
|
476
|
+
/* @__PURE__ */ jsx4(Track, { children: segments.map((segment, index) => /* @__PURE__ */ jsx4(
|
|
477
|
+
Box3,
|
|
478
|
+
{
|
|
479
|
+
sx: {
|
|
480
|
+
backgroundColor: colorOf(segment),
|
|
481
|
+
opacity: opacityOf(segment, index),
|
|
482
|
+
transition: animate ? `width 700ms ${EASE_OUT}` : "none",
|
|
483
|
+
"@media (prefers-reduced-motion: reduce)": { transition: "none" }
|
|
484
|
+
},
|
|
485
|
+
style: { width: `${loading || !total ? 0 : Math.max(0, segment.value) / total * 100}%` }
|
|
486
|
+
},
|
|
487
|
+
index
|
|
488
|
+
)) }),
|
|
489
|
+
/* @__PURE__ */ jsx4(Legend, { children: segments.map((segment, index) => /* @__PURE__ */ jsxs3(Box3, { component: "span", sx: { display: "inline-flex", alignItems: "center", gap: 0.75 }, children: [
|
|
490
|
+
/* @__PURE__ */ jsx4(
|
|
491
|
+
Box3,
|
|
492
|
+
{
|
|
493
|
+
component: "span",
|
|
494
|
+
"aria-hidden": true,
|
|
495
|
+
sx: { width: 6, height: 6, borderRadius: "50%", backgroundColor: colorOf(segment), opacity: opacityOf(segment, index) }
|
|
496
|
+
}
|
|
497
|
+
),
|
|
498
|
+
/* @__PURE__ */ jsx4(LegendValue, { value: segment.value, format, enabled: animate && !loading, duration }),
|
|
499
|
+
" ",
|
|
500
|
+
segment.label
|
|
501
|
+
] }, index)) })
|
|
502
|
+
] });
|
|
503
|
+
});
|
|
504
|
+
export {
|
|
505
|
+
KpiCard,
|
|
506
|
+
KpiCardGrid,
|
|
507
|
+
KpiSparkline,
|
|
508
|
+
KpiSplitBar,
|
|
509
|
+
formatCount,
|
|
510
|
+
formatDuration,
|
|
511
|
+
formatShare,
|
|
512
|
+
useCountUp
|
|
513
|
+
};
|
|
514
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/KpiCard.tsx","../src/KpiCard.constants.ts","../src/KpiSparkline.tsx","../src/format.ts","../src/useCountUp.ts","../src/visuallyHidden.ts","../src/KpiCardGrid.tsx","../src/KpiSplitBar.tsx"],"sourcesContent":["import { forwardRef, useContext, useMemo } from 'react';\nimport Box from '@mui/material/Box';\nimport { styled, type CSSObject, type Theme } from '@mui/material/styles';\nimport { tokens } from '@dax-ui/tokens';\nimport {\n cssVar,\n EASE_OUT,\n ENTER_DURATION,\n enter,\n growX,\n KpiCardContext,\n KpiGridSlotContext,\n pulse,\n SPARKLINE_HEIGHT,\n type KpiCardContextValue,\n} from './KpiCard.constants';\nimport type { KpiCardProps, KpiCardVariant, KpiDisplayValue, KpiTone } from './KpiCard.types';\nimport { KpiSparkline } from './KpiSparkline';\nimport { formatCount } from './format';\nimport { useCountUp } from './useCountUp';\nimport { visuallyHidden } from './visuallyHidden';\n\nconst { brand } = tokens.color;\n\n// The theme always has CSS variables (see @dax-ui/theme); the fallback only satisfies the types.\nconst palette = (theme: Theme) => (theme.vars ?? theme).palette;\n\nconst isSolid = (variant: KpiCardVariant) => variant === 'solid';\n\n/** The card's main color: the theme's primary for `brand`, else the matching palette color. */\nconst toneColor = (theme: Theme, tone: KpiTone) =>\n tone === 'brand' ? palette(theme).primary.main : palette(theme)[tone].main;\n\n// Plain surface shared by `elevated`, `outlined` and `accent`.\nconst surface = (theme: Theme) => ({\n color: palette(theme).text.primary,\n backgroundColor: palette(theme).background.paper,\n});\n\nfunction variantStyles(theme: Theme, variant: KpiCardVariant, tone: KpiTone): CSSObject {\n const main = toneColor(theme, tone);\n const ring = `inset 0 0 0 1px ${palette(theme).divider}`;\n\n switch (variant) {\n case 'solid':\n return {\n '--dax-kpi-accent': '#ffffff',\n '--dax-kpi-track': 'rgba(255, 255, 255, 0.15)',\n '--dax-kpi-muted': 'rgba(255, 255, 255, 0.85)',\n color: '#ffffff',\n // The same fill in light and dark mode.\n backgroundImage:\n tone === 'brand'\n ? `linear-gradient(150deg, ${brand[500]} 0%, ${brand[700]} 100%)`\n : `linear-gradient(150deg, ${main} 0%, ${palette(theme)[tone].dark} 100%)`,\n boxShadow: `inset 0 0 0 1px rgba(255, 255, 255, 0.3), 0 24px 48px -22px ${theme.alpha(main, 0.75)}`,\n };\n case 'elevated':\n return {\n ...surface(theme),\n boxShadow: '0 1px 2px rgba(15, 23, 42, 0.06), 0 12px 32px -12px rgba(15, 23, 42, 0.22)',\n '&:hover': {\n transform: 'translateY(-3px)',\n boxShadow: '0 2px 4px rgba(15, 23, 42, 0.06), 0 24px 48px -16px rgba(15, 23, 42, 0.32)',\n },\n ...theme.applyStyles('dark', {\n boxShadow: `${ring}, 0 16px 40px -16px rgba(0, 0, 0, 0.7)`,\n '&:hover': { transform: 'translateY(-3px)', boxShadow: `${ring}, 0 28px 56px -18px rgba(0, 0, 0, 0.8)` },\n }),\n };\n case 'tinted':\n return {\n color: palette(theme).text.primary,\n backgroundColor: theme.alpha(main, 0.08),\n boxShadow: `inset 0 0 0 1px ${theme.alpha(main, 0.16)}`,\n '&:hover': { backgroundColor: theme.alpha(main, 0.15) },\n };\n case 'outlined':\n return {\n ...surface(theme),\n boxShadow: ring,\n '&:hover': { boxShadow: `inset 0 0 0 1.5px ${main}` },\n };\n case 'accent':\n return {\n ...surface(theme),\n boxShadow: `${ring}, 0 8px 24px -16px rgba(15, 23, 42, 0.25)`,\n // The line along the top grows in from the left, then thickens on hover.\n '&::before': {\n content: '\"\"',\n position: 'absolute',\n insetInline: 0,\n top: 0,\n height: 4,\n backgroundColor: main,\n transformOrigin: 'left',\n transition: `height 250ms ${EASE_OUT}`,\n animation: `${growX} 700ms 150ms ${EASE_OUT} backwards`,\n '@media (prefers-reduced-motion: reduce)': { animation: 'none' },\n },\n '&:hover::before': { height: 6 },\n };\n case 'glass':\n default:\n return {\n color: palette(theme).text.primary,\n // Frosted: whatever sits behind the cards shows through, blurred.\n backgroundColor: theme.alpha(palette(theme).background.paper, 0.6),\n backdropFilter: 'blur(16px) saturate(1.4)',\n WebkitBackdropFilter: 'blur(16px) saturate(1.4)',\n boxShadow: `${ring}, inset 0 1px 0 rgba(255, 255, 255, 0.6), 0 20px 40px -26px rgba(15, 23, 42, 0.35)`,\n ...theme.applyStyles('dark', { boxShadow: `${ring}, 0 20px 40px -26px rgba(0, 0, 0, 0.6)` }),\n };\n }\n}\n\nconst Root = styled(Box, {\n shouldForwardProp: (p) => p !== 'variant' && p !== 'tone' && p !== 'animate' && p !== 'hasSparkline',\n})<{ variant: KpiCardVariant; tone: KpiTone; animate: boolean; hasSparkline: boolean }>(\n ({ theme, variant, tone, animate, hasSparkline }) => ({\n position: 'relative',\n isolation: 'isolate',\n overflow: 'hidden',\n display: 'flex',\n flexDirection: 'column',\n justifyContent: 'space-between',\n gap: 16,\n minWidth: 0,\n padding: 20,\n paddingBottom: hasSparkline ? 36 : 20,\n borderRadius: 20,\n transition: `transform 250ms ${EASE_OUT}, box-shadow 250ms ${EASE_OUT}, background-color 250ms ${EASE_OUT}`,\n // `solid` sets its own; skipping these keeps the dark-mode rule from overriding its white.\n ...(!isSolid(variant) && {\n '--dax-kpi-accent': tone === 'brand' ? palette(theme).primary.dark : toneColor(theme, tone),\n '--dax-kpi-track': theme.alpha(toneColor(theme, tone), 0.14),\n '--dax-kpi-muted': palette(theme).text.secondary,\n ...theme.applyStyles('dark', { '--dax-kpi-accent': toneColor(theme, tone) }),\n }),\n ...(animate && { animation: `${enter} ${ENTER_DURATION}ms ${EASE_OUT} backwards` }),\n ...variantStyles(theme, variant, tone),\n '@media (prefers-reduced-motion: reduce)': {\n animation: 'none',\n transition: 'none',\n '&:hover': { transform: 'none' },\n },\n }),\n);\n\n// Content sits above the sparkline, which is absolutely positioned behind it.\nconst Row = styled('div')({ position: 'relative', display: 'flex', alignItems: 'center', gap: 12 });\n\nconst Label = styled('span')({ fontSize: 13.5, fontWeight: 500, color: cssVar.muted });\n\nconst IconTile = styled('span', { shouldForwardProp: (p) => p !== 'variant' })<{ variant: KpiCardVariant }>(\n ({ theme, variant }) => ({\n display: 'flex',\n flexShrink: 0,\n alignItems: 'center',\n justifyContent: 'center',\n width: 34,\n height: 34,\n marginLeft: 'auto',\n borderRadius: 11,\n color: cssVar.accent,\n backgroundColor: isSolid(variant) ? 'rgba(255, 255, 255, 0.2)' : `color-mix(in srgb, ${cssVar.accent} 10%, transparent)`,\n '& svg': { width: 17, height: 17 },\n }),\n);\n\nconst Figure = styled('span', { shouldForwardProp: (p) => p !== 'loading' })<{ loading: boolean }>(({ loading }) => ({\n fontSize: 40,\n fontWeight: 700,\n lineHeight: 1,\n letterSpacing: '-0.03em',\n fontVariantNumeric: 'tabular-nums',\n color: cssVar.accent,\n transition: 'opacity 300ms',\n ...(loading && { opacity: 0.45, animation: `${pulse} 1.6s ease-in-out infinite` }),\n}));\n\nconst Unit = styled('span')({ marginLeft: 4, fontSize: 20, letterSpacing: '-0.02em', opacity: 0.7 });\n\n// Shown and hidden by fading, so the footer keeps its height and cards stay lined up.\nconst fadeWhileLoading = (loading: boolean) => ({ transition: 'opacity 500ms', opacity: loading ? 0 : 1 });\n\nconst Trend = styled('span', { shouldForwardProp: (p) => p !== 'tone' && p !== 'loading' })<{\n tone: 'success' | 'error' | 'onBrand';\n loading: boolean;\n}>(({ theme, tone, loading }) => ({\n display: 'inline-flex',\n alignItems: 'center',\n gap: 2,\n fontSize: 12,\n fontWeight: 600,\n color: tone === 'onBrand' ? '#ffffff' : palette(theme)[tone].main,\n '& svg': { width: 12, height: 12 },\n ...fadeWhileLoading(loading),\n}));\n\nconst Tag = styled('span', { shouldForwardProp: (p) => p !== 'tone' && p !== 'variant' && p !== 'loading' })<{\n tone: KpiTone;\n variant: KpiCardVariant;\n loading: boolean;\n}>(({ theme, tone, variant, loading }) => {\n const color = toneColor(theme, tone);\n return {\n padding: '2px 8px',\n borderRadius: 999,\n fontSize: 11,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n ...(isSolid(variant)\n ? { color: '#ffffff', backgroundColor: 'rgba(255, 255, 255, 0.2)' }\n : { color: tone === 'brand' ? cssVar.accent : color, backgroundColor: theme.alpha(color, 0.1) }),\n ...fadeWhileLoading(loading),\n };\n});\n\nconst Sparkline = styled(KpiSparkline)({\n position: 'absolute',\n insetInline: 0,\n bottom: 0,\n width: '100%',\n height: SPARKLINE_HEIGHT,\n});\n\nfunction Arrow({ direction }: { direction: 'up' | 'down' }) {\n return (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.6} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n {direction === 'up' ? <path d=\"M7 17 17 7M7 7h10v10\" /> : <path d=\"M7 7l10 10M17 7v10H7\" />}\n </svg>\n );\n}\n\nconst asText = (display: KpiDisplayValue) =>\n typeof display === 'string' ? display : [display.value, display.unit].filter(Boolean).join(' ');\n\n/**\n * A dashboard figure: label and icon, a big number that counts up, an optional trend,\n * a footer pill with a caption, and a faint sparkline. The cards slide in one after\n * another inside a `KpiCardGrid`.\n */\nexport const KpiCard = forwardRef<HTMLDivElement, KpiCardProps>(function KpiCard(\n {\n label,\n value,\n format = formatCount,\n decimals = 0,\n icon,\n trend,\n tag,\n tagTone = 'brand',\n caption,\n sparkline,\n showTrend = true,\n showTag = true,\n showSparkline = true,\n variant = 'glass',\n color = 'brand',\n loading = false,\n animate = true,\n countUpDuration = 900,\n enterDelay,\n children,\n style,\n ...rest\n },\n ref,\n) {\n const slot = useContext(KpiGridSlotContext);\n const delay = enterDelay ?? (slot ? slot.index * slot.stagger : 0);\n\n const shown = useCountUp(value, { enabled: animate && !loading, duration: countUpDuration, decimals });\n const display = format(shown);\n const figure = typeof display === 'string' ? { value: display } : display;\n\n const hasSparkline = showSparkline && Boolean(sparkline && sparkline.length > 1);\n const visibleTag = showTag ? tag : undefined;\n const showFooter = Boolean(visibleTag || caption) || !children;\n\n const trendTone =\n isSolid(variant)\n ? 'onBrand'\n : (trend?.sentiment ?? (trend?.direction === 'up' ? 'positive' : 'negative')) === 'positive'\n ? 'success'\n : 'error';\n\n const context = useMemo<KpiCardContextValue>(\n () => ({ variant, loading, animate, countUpDuration }),\n [variant, loading, animate, countUpDuration],\n );\n\n return (\n <KpiCardContext.Provider value={context}>\n <Root\n ref={ref}\n variant={variant}\n tone={color}\n animate={animate}\n hasSparkline={hasSparkline}\n aria-busy={loading || undefined}\n style={{ animationDelay: delay ? `${delay}ms` : undefined, ...style }}\n {...rest}\n >\n {hasSparkline && <Sparkline data={sparkline!} color={cssVar.accent} animate={animate} />}\n\n <Row>\n <Label>{label}</Label>\n {icon && <IconTile variant={variant}>{icon}</IconTile>}\n </Row>\n\n <Row sx={{ flexWrap: 'wrap', alignItems: 'baseline', columnGap: '10px', rowGap: '4px' }}>\n {/* The counting figure is hidden from screen readers, which get the final one once. */}\n <Figure loading={loading} aria-hidden>\n {figure.value}\n {figure.unit && <Unit>{figure.unit}</Unit>}\n </Figure>\n <span style={visuallyHidden}>{loading ? 'Loading' : asText(format(value))}</span>\n\n {trend && showTrend && (\n <Trend tone={trendTone} loading={loading}>\n <Arrow direction={trend.direction} />\n <span style={visuallyHidden}>{trend.direction === 'up' ? 'Up ' : 'Down '}</span>\n {trend.value}\n </Trend>\n )}\n </Row>\n\n {children && <Box sx={{ position: 'relative' }}>{children}</Box>}\n\n {showFooter && (\n <Row sx={{ minHeight: 22, gap: 1, fontSize: 12, color: cssVar.muted }}>\n {visibleTag && (\n <Tag tone={tagTone} variant={variant} loading={loading}>\n {visibleTag}\n </Tag>\n )}\n {caption}\n </Row>\n )}\n </Root>\n </KpiCardContext.Provider>\n );\n});\n","import { createContext } from 'react';\nimport { keyframes } from '@mui/material/styles';\nimport type { KpiCardVariant } from './KpiCard.types';\n\n/** Space at the bottom of a card with a sparkline, in px, so the line never runs behind text. */\nexport const SPARKLINE_HEIGHT = 64;\nexport const ENTER_DURATION = 500;\nexport const DEFAULT_STAGGER = 60;\n/** The curve every card motion uses: quick start, long soft landing. */\nexport const EASE_OUT = 'cubic-bezier(0.22, 1, 0.36, 1)';\n\n/**\n * Colors are applied as CSS variables on the card, so its parts (and a `KpiSplitBar`\n * inside it) follow the variant. `cssVar.accent` is `var(--dax-kpi-accent)`.\n */\nexport const cssVar = {\n /** Figure, icon, sparkline and the first split-bar segment. */\n accent: 'var(--dax-kpi-accent)',\n /** Unfilled part of a split bar. */\n track: 'var(--dax-kpi-track)',\n /** Label, caption and legend text. */\n muted: 'var(--dax-kpi-muted)',\n};\n\nexport const enter = keyframes`\n from { opacity: 0; transform: translateY(12px); }\n`;\n\n// Paths are drawn with pathLength={1}, so a dash of 1 is the whole line.\nexport const draw = keyframes`\n from { stroke-dashoffset: 1; }\n to { stroke-dashoffset: 0; }\n`;\n\nexport const fadeIn = keyframes`\n from { opacity: 0; }\n`;\n\nexport const growX = keyframes`\n from { transform: scaleX(0); }\n`;\n\nexport const pulse = keyframes`\n 50% { opacity: 0.45; }\n`;\n\nexport interface KpiCardContextValue {\n variant: KpiCardVariant;\n loading: boolean;\n animate: boolean;\n countUpDuration: number;\n}\n\n/** Lets a `KpiSplitBar` inside a card follow its loading and animation state. */\nexport const KpiCardContext = createContext<KpiCardContextValue | null>(null);\n\n/** Position of a card inside a `KpiCardGrid`, which sets its default `enterDelay`. */\nexport const KpiGridSlotContext = createContext<{ index: number; stagger: number } | null>(null);\n","import { styled } from '@mui/material/styles';\nimport { draw, EASE_OUT, fadeIn } from './KpiCard.constants';\nimport type { KpiSparklineProps } from './KpiCard.types';\n\nconst WIDTH = 200;\nconst HEIGHT = 60;\n\nconst Svg = styled('svg', { shouldForwardProp: (p) => p !== 'animate' })<{ animate: boolean }>(({ animate }) => ({\n display: 'block',\n pointerEvents: 'none',\n overflow: 'visible',\n ...(animate && {\n '& .dax-kpi-spark-line': { strokeDasharray: 1, animation: `${draw} 1.1s ${EASE_OUT} backwards` },\n '& .dax-kpi-spark-area': { animation: `${fadeIn} 0.9s 0.35s ease-out backwards` },\n // A line drawing itself is decoration; reduced motion gets the finished shape.\n '@media (prefers-reduced-motion: reduce)': {\n '& .dax-kpi-spark-line, & .dax-kpi-spark-area': { animation: 'none', strokeDasharray: 'none' },\n },\n }),\n}));\n\n/** Line and area paths for `data` in the 200×60 box. The line stays in the bottom strip (y 32–58). */\nfunction paths(data: number[]) {\n const max = Math.max(...data);\n const min = Math.min(...data);\n const span = max - min || 1;\n const steps = data.length - 1 || 1;\n\n const line = data\n .map((value, index) => {\n const x = (index / steps) * WIDTH;\n const y = 58 - ((value - min) / span) * 26;\n return `${index ? 'L' : 'M'}${x.toFixed(1)} ${y.toFixed(1)}`;\n })\n .join(' ');\n\n return { line, area: `${line} L${WIDTH} ${HEIGHT} L0 ${HEIGHT} Z` };\n}\n\n/**\n * A small decorative trend line with a faint fill. Stretches to its box; `KpiCard` lays it\n * along its bottom edge. Hidden from screen readers.\n */\nexport function KpiSparkline({ data, color = 'currentColor', animate = true, className }: KpiSparklineProps) {\n if (data.length < 2) return null;\n const { line, area } = paths(data);\n\n return (\n <Svg\n viewBox={`0 0 ${WIDTH} ${HEIGHT}`}\n preserveAspectRatio=\"none\"\n aria-hidden\n focusable=\"false\"\n animate={animate}\n className={className}\n >\n <path d={area} pathLength={1} className=\"dax-kpi-spark-area\" fill={color} fillOpacity={0.08} />\n <path\n d={line}\n pathLength={1}\n className=\"dax-kpi-spark-line\"\n fill=\"none\"\n stroke={color}\n strokeOpacity={0.35}\n strokeWidth={1.6}\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n />\n </Svg>\n );\n}\n","import type { KpiDisplayValue } from './KpiCard.types';\n\n/** A whole number with thousands separators: 93420 → \"93,420\". */\nexport function formatCount(value: number): string {\n return Math.round(value).toLocaleString('en-US');\n}\n\n/**\n * Seconds as the two largest units, the second shown smaller by `KpiCard`:\n * 93420 → \"25h\" + \"57m\", 292 → \"4m\" + \"52s\", 40 → \"40s\".\n */\nexport function formatDuration(seconds: number): Exclude<KpiDisplayValue, string> {\n const total = Math.max(0, Math.round(seconds));\n const hours = Math.floor(total / 3600);\n const minutes = Math.floor((total % 3600) / 60);\n\n if (hours > 0) return { value: `${formatCount(hours)}h`, unit: `${minutes}m` };\n if (minutes > 0) return { value: `${minutes}m`, unit: `${total % 60}s` };\n return { value: `${total}s` };\n}\n\n/** `part` as a share of `whole`: (42, 100) → \"42.0%\". `undefined` when either is zero, so there's no \"0%\" pill. */\nexport function formatShare(part: number, whole: number, decimals = 1): string | undefined {\n if (!whole || !part) return undefined;\n return `${((part / whole) * 100).toFixed(decimals)}%`;\n}\n","import { useEffect, useRef, useState } from 'react';\n\nexport interface UseCountUpOptions {\n /**\n * Off while the figure is unknown, so nothing counts toward a guess. The figure is\n * shown as is.\n * @default true\n */\n enabled?: boolean;\n /**\n * Length of the count, in ms.\n * @default 900\n */\n duration?: number;\n /**\n * Decimal places to round to on every frame.\n * @default 0\n */\n decimals?: number;\n}\n\n// Fast out of the gate, settling onto the figure rather than stopping dead.\nconst easeOutExpo = (t: number) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t));\n\nconst roundTo = (n: number, decimals: number) => Number(n.toFixed(decimals));\n\nconst prefersReducedMotion = () =>\n typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\n/**\n * Counts from the figure on screen to `value`: from 0 on mount, then from the old figure\n * to the new one, so changing a filter nudges figures instead of restarting them at 0.\n * Every frame is rounded to `decimals`, and the last frame is `value` exactly.\n * Returns `value` straight away for viewers who ask for reduced motion.\n */\nexport function useCountUp(value: number, { enabled = true, duration = 900, decimals = 0 }: UseCountUpOptions = {}): number {\n const [shown, setShown] = useState(() => (enabled && !prefersReducedMotion() ? 0 : roundTo(value, decimals)));\n // The exact, unrounded position, so a count cut short by a new value carries on from it.\n const fromRef = useRef(0);\n\n useEffect(() => {\n const target = Number.isFinite(value) ? value : 0;\n\n if (!enabled || duration <= 0 || prefersReducedMotion()) {\n fromRef.current = target;\n setShown(roundTo(target, decimals));\n return;\n }\n\n const from = fromRef.current;\n if (from === target) {\n setShown(roundTo(target, decimals));\n return;\n }\n\n const start = performance.now();\n // The clock is read here rather than taken from the frame's timestamp, which not\n // every environment measures from the same origin as performance.now().\n let frame = requestAnimationFrame(function tick() {\n const progress = Math.min(1, (performance.now() - start) / duration);\n const next = progress === 1 ? target : from + (target - from) * easeOutExpo(progress);\n fromRef.current = next;\n setShown(roundTo(next, decimals));\n if (progress < 1) frame = requestAnimationFrame(tick);\n });\n\n return () => cancelAnimationFrame(frame);\n }, [value, enabled, duration, decimals]);\n\n return shown;\n}\n","/** Hides content visually while keeping it available to screen readers. */\nexport const visuallyHidden = {\n position: 'absolute',\n width: 1,\n height: 1,\n margin: -1,\n padding: 0,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n} as const;\n","import { Children, forwardRef } from 'react';\nimport Box from '@mui/material/Box';\nimport { DEFAULT_STAGGER, KpiGridSlotContext } from './KpiCard.constants';\nimport type { KpiCardGridProps } from './KpiCard.types';\n\nconst DEFAULT_COLUMNS = { xs: 1, sm: 2, xl: 4 };\n\n/**\n * Lays `KpiCard`s out in a responsive grid and brings them in one after another,\n * `stagger` ms apart, so a row of figures reads as a sweep rather than a flash.\n */\nexport const KpiCardGrid = forwardRef<HTMLDivElement, KpiCardGridProps>(function KpiCardGrid(\n { columns = DEFAULT_COLUMNS, stagger = DEFAULT_STAGGER, children, sx, ...rest },\n ref,\n) {\n const templateColumns =\n typeof columns === 'number'\n ? `repeat(${columns}, minmax(0, 1fr))`\n : Object.fromEntries(Object.entries(columns).map(([bp, n]) => [bp, `repeat(${n}, minmax(0, 1fr))`]));\n\n return (\n <Box\n ref={ref}\n sx={[{ display: 'grid', gap: 2, gridTemplateColumns: templateColumns }, ...(Array.isArray(sx) ? sx : [sx])]}\n {...rest}\n >\n {/* toArray drops null and false children, so a hidden card doesn't leave a gap in the sequence. */}\n {Children.toArray(children).map((child, index) => (\n <KpiGridSlotContext.Provider key={index} value={{ index, stagger }}>\n {child}\n </KpiGridSlotContext.Provider>\n ))}\n </Box>\n );\n});\n","import { forwardRef, useContext } from 'react';\nimport Box from '@mui/material/Box';\nimport { styled } from '@mui/material/styles';\nimport { cssVar, EASE_OUT, KpiCardContext } from './KpiCard.constants';\nimport type { KpiSplitBarProps, KpiSplitBarSegment } from './KpiCard.types';\nimport { formatCount } from './format';\nimport { useCountUp } from './useCountUp';\n\n// Later segments without their own color fade out from the card's accent.\nconst SEGMENT_OPACITY = [1, 0.45, 0.25, 0.15];\n\nconst Track = styled('div')({\n display: 'flex',\n gap: 2,\n height: 6,\n overflow: 'hidden',\n borderRadius: 999,\n backgroundColor: cssVar.track,\n});\n\nconst Legend = styled('div')({\n display: 'flex',\n flexWrap: 'wrap',\n justifyContent: 'space-between',\n gap: '4px 12px',\n marginTop: 8,\n fontSize: 11,\n color: cssVar.muted,\n fontVariantNumeric: 'tabular-nums',\n});\n\nfunction LegendValue({ value, format, enabled, duration }: { value: number; format: (n: number) => string; enabled: boolean; duration: number }) {\n return <>{format(useCountUp(value, { enabled, duration }))}</>;\n}\n\n/**\n * A single bar split into the parts of a whole, with a legend of counts underneath,\n * e.g. AI against manual calls. The segments grow in from zero. Inside a `KpiCard` it\n * takes the card's colors and waits for its `loading` to finish.\n */\nexport const KpiSplitBar = forwardRef<HTMLDivElement, KpiSplitBarProps>(function KpiSplitBar(\n { segments, format = formatCount, ...rest },\n ref,\n) {\n const card = useContext(KpiCardContext);\n const loading = card?.loading ?? false;\n const animate = card?.animate ?? true;\n const duration = card?.countUpDuration ?? 900;\n // Guard the divide: a whole of zero would split 0/0 into NaN%.\n const total = segments.reduce((sum, s) => sum + Math.max(0, s.value), 0);\n\n const colorOf = (segment: KpiSplitBarSegment) => segment.color ?? cssVar.accent;\n const opacityOf = (segment: KpiSplitBarSegment, index: number) => (segment.color ? 1 : (SEGMENT_OPACITY[index] ?? 0.15));\n\n return (\n <Box ref={ref} {...rest}>\n <Track>\n {segments.map((segment, index) => (\n <Box\n key={index}\n sx={{\n backgroundColor: colorOf(segment),\n opacity: opacityOf(segment, index),\n transition: animate ? `width 700ms ${EASE_OUT}` : 'none',\n '@media (prefers-reduced-motion: reduce)': { transition: 'none' },\n }}\n style={{ width: `${loading || !total ? 0 : (Math.max(0, segment.value) / total) * 100}%` }}\n />\n ))}\n </Track>\n <Legend>\n {segments.map((segment, index) => (\n <Box key={index} component=\"span\" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}>\n <Box\n component=\"span\"\n aria-hidden\n sx={{ width: 6, height: 6, borderRadius: '50%', backgroundColor: colorOf(segment), opacity: opacityOf(segment, index) }}\n />\n <LegendValue value={segment.value} format={format} enabled={animate && !loading} duration={duration} /> {segment.label}\n </Box>\n ))}\n </Legend>\n </Box>\n );\n});\n"],"mappings":"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","names":["styled","jsx","jsxs","styled","KpiCard","forwardRef","Box","jsx","forwardRef","KpiCardGrid","Box","forwardRef","useContext","Box","styled","jsx","jsxs","styled","forwardRef","KpiSplitBar","useContext","Box"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dax-ui/kpi-card",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "dax-ui KpiCard: animated dashboard figures with trends, sparklines and split bars",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dax-ui",
|
|
7
|
+
"react",
|
|
8
|
+
"design-system",
|
|
9
|
+
"mui",
|
|
10
|
+
"kpi-card",
|
|
11
|
+
"kpi",
|
|
12
|
+
"dashboard",
|
|
13
|
+
"stat-card"
|
|
14
|
+
],
|
|
15
|
+
"author": "Digital Auxilius",
|
|
16
|
+
"homepage": "https://github.com/FaaizHussain26/DAX-UI/tree/main/packages/kpi-card#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/FaaizHussain26/DAX-UI.git",
|
|
20
|
+
"directory": "packages/kpi-card"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/FaaizHussain26/DAX-UI/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "UNLICENSED",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "lib/index.js",
|
|
29
|
+
"types": "lib/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./lib/index.d.ts",
|
|
33
|
+
"default": "./lib/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"lib",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@emotion/react": "^11.14.0",
|
|
43
|
+
"@emotion/styled": "^11.14.0",
|
|
44
|
+
"@mui/material": "^9.5.0",
|
|
45
|
+
"@dax-ui/tokens": "^0.4.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
49
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@dax-ui/builder": "^0.0.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "builder"
|
|
59
|
+
}
|
|
6
60
|
}
|