@seekalfred/primitives 0.7.1 → 0.7.4
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/README.md +255 -5
- package/dist/alert/alert.d.ts +49 -4
- package/dist/alert/alert.js +66 -38
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert/index.d.ts +2 -2
- package/dist/alert/index.js +2 -2
- package/dist/alert-dialog/alert-dialog.d.ts +31 -4
- package/dist/alert-dialog/alert-dialog.js +75 -26
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/announcement-bar/announcement-bar.d.ts +15 -4
- package/dist/announcement-bar/announcement-bar.js +31 -6
- package/dist/announcement-bar/announcement-bar.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -0
- package/dist/avatar/avatar.js +33 -12
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart-series.d.ts +75 -0
- package/dist/bar-chart/bar-chart-series.js +401 -0
- package/dist/bar-chart/bar-chart-series.js.map +1 -0
- package/dist/bar-chart/bar-chart.d.ts +25 -1
- package/dist/bar-chart/bar-chart.js +26 -1
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -1
- package/dist/button/button.d.ts +1 -1
- package/dist/calendar/calendar.d.ts +1 -1
- package/dist/card/card.d.ts +22 -1
- package/dist/card/card.js +60 -7
- package/dist/card/card.js.map +1 -1
- package/dist/carousel/carousel.d.ts +21 -0
- package/dist/carousel/carousel.js +34 -8
- package/dist/carousel/carousel.js.map +1 -1
- package/dist/chip-group/chip-group.d.ts +11 -0
- package/dist/chip-group/chip-group.js +50 -34
- package/dist/chip-group/chip-group.js.map +1 -1
- package/dist/chip-group/index.d.ts +2 -2
- package/dist/copy-button/copy-button.d.ts +37 -5
- package/dist/copy-button/copy-button.js +35 -11
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +22 -0
- package/dist/donut-chart/donut-chart.js +35 -18
- package/dist/donut-chart/donut-chart.js.map +1 -1
- package/dist/donut-chart/index.d.ts +2 -2
- package/dist/empty-state/empty-state.d.ts +21 -0
- package/dist/empty-state/empty-state.js +16 -4
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
- package/dist/file-drop-zone/file-drop-zone.js +26 -8
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/filter-bar/filter-bar.d.ts +4 -1
- package/dist/filter-bar/filter-bar.js +53 -9
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/gauge/gauge.d.ts +18 -1
- package/dist/gauge/gauge.js +35 -21
- package/dist/gauge/gauge.js.map +1 -1
- package/dist/gauge/index.d.ts +2 -2
- package/dist/index.d.ts +15 -14
- package/dist/index.js +9 -9
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +7 -5
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +6 -3
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/field-surface.js +22 -1
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/lib/use-copy-feedback.js +17 -3
- package/dist/lib/use-copy-feedback.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +144 -2
- package/dist/line-chart/line-chart.js +314 -46
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +36 -1
- package/dist/money-input/money-input.js +30 -14
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +26 -1
- package/dist/morph-select/morph-select.js +28 -6
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/index.d.ts +2 -2
- package/dist/multi-select/index.js +2 -2
- package/dist/multi-select/multi-select.d.ts +74 -2
- package/dist/multi-select/multi-select.js +213 -39
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/password-field/password-field.d.ts +16 -0
- package/dist/password-field/password-field.js +37 -8
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/popover/index.d.ts +2 -2
- package/dist/popover/index.js +2 -2
- package/dist/popover/popover.d.ts +39 -2
- package/dist/popover/popover.js +29 -2
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/index.js +2 -2
- package/dist/progress/progress.d.ts +68 -3
- package/dist/progress/progress.js +156 -39
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/index.d.ts +2 -2
- package/dist/radio-group/index.js +2 -2
- package/dist/radio-group/radio-group.d.ts +33 -3
- package/dist/radio-group/radio-group.js +109 -9
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/scroll-area/scroll-area.js +4 -3
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/scrub-input/scrub-input.d.ts +11 -1
- package/dist/scrub-input/scrub-input.js +22 -10
- package/dist/scrub-input/scrub-input.js.map +1 -1
- package/dist/search-field/search-field.d.ts +22 -1
- package/dist/search-field/search-field.js +22 -7
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +10 -1
- package/dist/select/select.js +11 -8
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +21 -9
- package/dist/separator/separator.js +33 -11
- package/dist/separator/separator.js.map +1 -1
- package/dist/sparkline/sparkline.d.ts +27 -2
- package/dist/sparkline/sparkline.js +115 -18
- package/dist/sparkline/sparkline.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +65 -1
- package/dist/stat-card/stat-card.js +98 -25
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +115 -5
- package/dist/table/table.js +374 -94
- package/dist/table/table.js.map +1 -1
- package/dist/text-shimmer/text-shimmer.d.ts +18 -2
- package/dist/text-shimmer/text-shimmer.js +5 -2
- package/dist/text-shimmer/text-shimmer.js.map +1 -1
- package/dist/textarea/textarea.d.ts +7 -0
- package/dist/textarea/textarea.js +3 -1
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/time-picker/time-picker.d.ts +36 -1
- package/dist/time-picker/time-picker.js +33 -5
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/usage-meter/index.d.ts +2 -2
- package/dist/usage-meter/usage-meter.d.ts +30 -0
- package/dist/usage-meter/usage-meter.js +56 -38
- package/dist/usage-meter/usage-meter.js.map +1 -1
- package/package.json +3 -3
|
@@ -24,11 +24,21 @@ import { ReactNode } from "react";
|
|
|
24
24
|
* colour. `neutral` is the default and is right for any figure without a
|
|
25
25
|
* verdict in it, including every zero.
|
|
26
26
|
* - Cards in a row are the same height (`h-full` in a grid or flex row).
|
|
27
|
+
* - The change pill's colour is what the change MEANS. By default a rise is
|
|
28
|
+
* good; `trend="inverse"` reads a rise as bad (bounce rate, cost, keywords
|
|
29
|
+
* that dropped out), `trend="neutral"` gives no verdict, and `changeTone`
|
|
30
|
+
* names the meaning outright when the text carries no sign.
|
|
27
31
|
*
|
|
28
32
|
* A card can be a link (`href`, React Aria Link, so a router provider routes
|
|
29
33
|
* it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a
|
|
30
34
|
* React Aria ToggleButton with `aria-pressed` and one shared selected look).
|
|
31
35
|
*
|
|
36
|
+
* `isLoading` keeps the card's box while its figure is fetched: the label
|
|
37
|
+
* stays (or pulses as a placeholder when it is empty), the figure and the
|
|
38
|
+
* context line become placeholder blocks the height of the lines they stand
|
|
39
|
+
* in for, and the change pill pulses at its own size. So a row of loading cards is exactly as
|
|
40
|
+
* tall as the row that replaces it and nothing shifts when the data lands.
|
|
41
|
+
*
|
|
32
42
|
* Every string in the cva blocks below is a STATIC literal. Tailwind scans
|
|
33
43
|
* source text, so a class assembled at runtime is invisible to it and would
|
|
34
44
|
* simply never be emitted.
|
|
@@ -41,10 +51,24 @@ export declare const statCardVariants: (props?: ({
|
|
|
41
51
|
export declare const statCardHeaderVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
42
52
|
export declare const statCardLabelVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
43
53
|
export declare const statCardValueVariants: (props?: ({
|
|
44
|
-
tone?: "neutral" | "
|
|
54
|
+
tone?: "neutral" | "positive" | "negative" | "warning" | "attention" | null | undefined;
|
|
45
55
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
56
|
+
/** An optional icon before the label. Decorative: the label names the figure. */
|
|
57
|
+
export declare const statCardIconVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
46
58
|
export declare const statCardSubVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
59
|
+
/**
|
|
60
|
+
* A placeholder standing in for one line of copy while the card loads. It sits
|
|
61
|
+
* inside the line it replaces and is one line-height tall (`1lh`), so the line
|
|
62
|
+
* box, and with it the card, keeps the height of the loaded copy.
|
|
63
|
+
*/
|
|
64
|
+
export declare const statCardSkeletonVariants: (props?: ({
|
|
65
|
+
part?: "label" | "value" | "sub" | null | undefined;
|
|
66
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
47
67
|
export type StatCardTone = "neutral" | "positive" | "negative" | "warning" | "attention";
|
|
68
|
+
/** Which way is good for this figure: up (`normal`), down (`inverse`), or neither (`neutral`). */
|
|
69
|
+
export type StatCardTrend = "normal" | "inverse" | "neutral";
|
|
70
|
+
/** What a change means, whatever its sign. */
|
|
71
|
+
export type StatCardChangeTone = "positive" | "negative" | "neutral";
|
|
48
72
|
export interface StatCardProps {
|
|
49
73
|
/** What the figure is. One line; cut with an ellipsis, full text on hover. */
|
|
50
74
|
label: string;
|
|
@@ -63,6 +87,27 @@ export interface StatCardProps {
|
|
|
63
87
|
* place and the pill follows its width.
|
|
64
88
|
*/
|
|
65
89
|
change?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Which direction of `change` is good. `normal` (default): a leading `+`
|
|
92
|
+
* reads green and a leading `-` red. `inverse`: a rise reads red and a fall
|
|
93
|
+
* green, for a figure where more is worse (bounce rate, cost per click,
|
|
94
|
+
* keywords that fell out of the top 100). `neutral`: no colour either way.
|
|
95
|
+
* @default "normal"
|
|
96
|
+
*/
|
|
97
|
+
trend?: StatCardTrend;
|
|
98
|
+
/**
|
|
99
|
+
* The meaning of `change`, named outright. Wins over `trend` and the sign;
|
|
100
|
+
* use it when the text has no `+` or `-` (`"▲4"`) or the verdict comes from
|
|
101
|
+
* elsewhere. It colours only the change pill, never the value (that is
|
|
102
|
+
* `tone`).
|
|
103
|
+
*/
|
|
104
|
+
changeTone?: StatCardChangeTone;
|
|
105
|
+
/**
|
|
106
|
+
* An icon before the label. Decorative (hidden from assistive technology).
|
|
107
|
+
* An SVG is drawn at 16px, whatever size it is given. A falsy value
|
|
108
|
+
* (`false`, `null`, `""`) renders nothing, so `icon={cond && <Icon />}` works.
|
|
109
|
+
*/
|
|
110
|
+
icon?: ReactNode;
|
|
66
111
|
/**
|
|
67
112
|
* What the figure means: `neutral` (default, and right for every zero),
|
|
68
113
|
* `positive`, `negative`, `warning` (part-way or waiting) or `attention`
|
|
@@ -80,11 +125,30 @@ export interface StatCardProps {
|
|
|
80
125
|
onPress?: () => void;
|
|
81
126
|
/** Hover text for the whole card. */
|
|
82
127
|
description?: string;
|
|
128
|
+
/**
|
|
129
|
+
* The figure is still loading. The card keeps its box: the label stays (a
|
|
130
|
+
* placeholder pulses in its place when it is `""`), the value and, when
|
|
131
|
+
* `sub` is given, the context line become placeholder blocks the height of
|
|
132
|
+
* those lines, and the change pill becomes a placeholder of its size. The card's content carries
|
|
133
|
+
* `aria-busy`, the root `data-loading`, and assistive technology reads
|
|
134
|
+
* `loadingLabel` in place of the figure. A link, button or toggle card stays
|
|
135
|
+
* operable. Pass `sub` (any value, even `""`) while loading to keep the
|
|
136
|
+
* context line's height.
|
|
137
|
+
* @default false
|
|
138
|
+
*/
|
|
139
|
+
isLoading?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Read by assistive technology in place of the figure while `isLoading`.
|
|
142
|
+
* @default "Loading"
|
|
143
|
+
*/
|
|
144
|
+
loadingLabel?: string;
|
|
83
145
|
/** Anything under the context line. */
|
|
84
146
|
children?: ReactNode;
|
|
85
147
|
/** Layout classes for the card (margins, grid placement). */
|
|
86
148
|
className?: string;
|
|
87
149
|
"aria-label"?: string;
|
|
150
|
+
/** `data-*` attributes land on the card's root (test ids, analytics hooks). `data-slot` stays the card's own. */
|
|
151
|
+
[dataAttribute: `data-${string}`]: string | number | boolean | undefined;
|
|
88
152
|
}
|
|
89
153
|
/**
|
|
90
154
|
* One headline figure.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { cn, cnAll } from "../lib/cn.js";
|
|
3
3
|
import { SwapText } from "../lib/swap-text.js";
|
|
4
4
|
import { AnimatedCounter } from "../animated-counter/animated-counter.js";
|
|
5
5
|
import { Badge } from "../badge/badge.js";
|
|
6
|
+
import { skeletonVariants } from "../skeleton/skeleton.js";
|
|
6
7
|
import { cva } from "class-variance-authority";
|
|
7
8
|
import { forwardRef, useState } from "react";
|
|
8
9
|
import { Button, Link, ToggleButton } from "react-aria-components";
|
|
9
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
10
11
|
//#region src/stat-card/stat-card.tsx
|
|
11
12
|
/**
|
|
12
13
|
* StatCard: one headline figure with its label and a line of context. A row
|
|
@@ -32,11 +33,21 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
32
33
|
* colour. `neutral` is the default and is right for any figure without a
|
|
33
34
|
* verdict in it, including every zero.
|
|
34
35
|
* - Cards in a row are the same height (`h-full` in a grid or flex row).
|
|
36
|
+
* - The change pill's colour is what the change MEANS. By default a rise is
|
|
37
|
+
* good; `trend="inverse"` reads a rise as bad (bounce rate, cost, keywords
|
|
38
|
+
* that dropped out), `trend="neutral"` gives no verdict, and `changeTone`
|
|
39
|
+
* names the meaning outright when the text carries no sign.
|
|
35
40
|
*
|
|
36
41
|
* A card can be a link (`href`, React Aria Link, so a router provider routes
|
|
37
42
|
* it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a
|
|
38
43
|
* React Aria ToggleButton with `aria-pressed` and one shared selected look).
|
|
39
44
|
*
|
|
45
|
+
* `isLoading` keeps the card's box while its figure is fetched: the label
|
|
46
|
+
* stays (or pulses as a placeholder when it is empty), the figure and the
|
|
47
|
+
* context line become placeholder blocks the height of the lines they stand
|
|
48
|
+
* in for, and the change pill pulses at its own size. So a row of loading cards is exactly as
|
|
49
|
+
* tall as the row that replaces it and nothing shifts when the data lands.
|
|
50
|
+
*
|
|
40
51
|
* Every string in the cva blocks below is a STATIC literal. Tailwind scans
|
|
41
52
|
* source text, so a class assembled at runtime is invisible to it and would
|
|
42
53
|
* simply never be emitted.
|
|
@@ -85,7 +96,21 @@ tone: {
|
|
|
85
96
|
} },
|
|
86
97
|
defaultVariants: { tone: "neutral" }
|
|
87
98
|
});
|
|
99
|
+
/** An optional icon before the label. Decorative: the label names the figure. */
|
|
100
|
+
const statCardIconVariants = cva("flex shrink-0 items-center text-muted-foreground [&_svg]:size-4 [&_svg]:shrink-0");
|
|
88
101
|
const statCardSubVariants = cva("m-0 mt-4 text-sm leading-(--leading-normal) text-muted-foreground");
|
|
102
|
+
/**
|
|
103
|
+
* A placeholder standing in for one line of copy while the card loads. It sits
|
|
104
|
+
* inside the line it replaces and is one line-height tall (`1lh`), so the line
|
|
105
|
+
* box, and with it the card, keeps the height of the loaded copy.
|
|
106
|
+
*/
|
|
107
|
+
const statCardSkeletonVariants = cva("inline-block h-[1lh] max-w-full align-top", { variants: {
|
|
108
|
+
/** Which line it stands in for: each has the rough width of its copy. */
|
|
109
|
+
part: {
|
|
110
|
+
label: "w-24",
|
|
111
|
+
value: "w-28",
|
|
112
|
+
sub: "w-36"
|
|
113
|
+
} } });
|
|
89
114
|
/** The number in a piece of text, for the direction a swap moves: a larger one rises from below. */
|
|
90
115
|
const amountIn = (text) => Number(text.replace(/,/g, "").match(/-?\d+(?:\.\d+)?/)?.[0] ?? NaN);
|
|
91
116
|
/** 1 when the new text holds a number that did not shrink, -1 when it did. Updated during render. */
|
|
@@ -144,7 +169,22 @@ function figureOf(value) {
|
|
|
144
169
|
suffix
|
|
145
170
|
};
|
|
146
171
|
}
|
|
147
|
-
const
|
|
172
|
+
const signOf = (change) => /^\+/.test(change) ? 1 : /^[-−]/.test(change) ? -1 : 0;
|
|
173
|
+
/** What a change means: the explicit tone, else its sign read the way `trend` says. */
|
|
174
|
+
function changeMeaning(change, trend, explicit) {
|
|
175
|
+
if (explicit) return explicit;
|
|
176
|
+
const sign = trend === "neutral" ? 0 : signOf(change) * (trend === "inverse" ? -1 : 1);
|
|
177
|
+
return sign > 0 ? "positive" : sign < 0 ? "negative" : "neutral";
|
|
178
|
+
}
|
|
179
|
+
const BADGE_TONE = {
|
|
180
|
+
positive: "success",
|
|
181
|
+
negative: "danger",
|
|
182
|
+
neutral: "neutral"
|
|
183
|
+
};
|
|
184
|
+
/** The swap key of an empty label's placeholder: a key no label text can be. */
|
|
185
|
+
const LOADING_KEY = "\0loading";
|
|
186
|
+
/** Only the `data-*` attributes, so nothing else in the rest object reaches the DOM. */
|
|
187
|
+
const dataAttributes = (rest) => Object.fromEntries(Object.entries(rest).filter(([key]) => key.startsWith("data-")));
|
|
148
188
|
/**
|
|
149
189
|
* One headline figure.
|
|
150
190
|
*
|
|
@@ -155,39 +195,70 @@ const changeTone = (change) => /^\+/.test(change) ? "success" : /^[-−]/.test(c
|
|
|
155
195
|
* <StatCard label="Collected" value="$9,800" tone="positive" href="/payments" />
|
|
156
196
|
* </div>
|
|
157
197
|
*/
|
|
158
|
-
const StatCard = forwardRef(function StatCard({ label, value, sub, change, tone, isSelected, href, onPress, description, children, className, ...rest }, ref) {
|
|
198
|
+
const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend = "normal", changeTone, icon, tone, isSelected, href, onPress, description, isLoading = false, loadingLabel = "Loading", children, className, ...rest }, ref) {
|
|
159
199
|
const figure = figureOf(value);
|
|
200
|
+
const placeholder = (part) => /* @__PURE__ */ jsx("span", {
|
|
201
|
+
"data-slot": "stat-card-skeleton",
|
|
202
|
+
"aria-hidden": "true",
|
|
203
|
+
className: cnAll(skeletonVariants(), statCardSkeletonVariants({ part }))
|
|
204
|
+
});
|
|
205
|
+
const meaning = change ? changeMeaning(change, trend, changeTone) : "neutral";
|
|
160
206
|
const body = /* @__PURE__ */ jsxs("span", {
|
|
207
|
+
"data-slot": "stat-card-body",
|
|
208
|
+
"aria-busy": isLoading || void 0,
|
|
161
209
|
className: "flex min-w-0 flex-1 flex-col",
|
|
162
210
|
title: description,
|
|
163
211
|
children: [
|
|
164
212
|
/* @__PURE__ */ jsxs("span", {
|
|
165
213
|
className: statCardHeaderVariants(),
|
|
166
|
-
children: [
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
214
|
+
children: [
|
|
215
|
+
icon ? /* @__PURE__ */ jsx("span", {
|
|
216
|
+
"data-slot": "stat-card-icon",
|
|
217
|
+
"aria-hidden": "true",
|
|
218
|
+
className: statCardIconVariants(),
|
|
219
|
+
children: icon
|
|
220
|
+
}) : null,
|
|
221
|
+
/* @__PURE__ */ jsx("span", {
|
|
222
|
+
"data-slot": "stat-card-label",
|
|
223
|
+
className: cn(statCardLabelVariants(), "block"),
|
|
224
|
+
children: /* @__PURE__ */ jsx(Swap, {
|
|
225
|
+
text: isLoading && label === "" ? LOADING_KEY : label,
|
|
226
|
+
className: "*:max-w-full",
|
|
227
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
228
|
+
className: "block truncate",
|
|
229
|
+
title: label,
|
|
230
|
+
children: isLoading && label === "" ? placeholder("label") : label
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
}),
|
|
234
|
+
change && isLoading ? /* @__PURE__ */ jsx("span", {
|
|
235
|
+
"data-slot": "stat-card-skeleton",
|
|
236
|
+
"aria-hidden": "true",
|
|
237
|
+
className: "box-border inline-flex shrink-0 rounded-pill bg-muted motion-safe:animate-skeleton-pulse",
|
|
238
|
+
children: /* @__PURE__ */ jsx(Badge, {
|
|
239
|
+
shape: "pill",
|
|
240
|
+
tone: "neutral",
|
|
241
|
+
appearance: "outline",
|
|
242
|
+
className: "invisible tabular-nums",
|
|
243
|
+
children: change
|
|
176
244
|
})
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
245
|
+
}) : change ? /* @__PURE__ */ jsx(Badge, {
|
|
246
|
+
shape: "pill",
|
|
247
|
+
tone: BADGE_TONE[meaning],
|
|
248
|
+
appearance: meaning === "neutral" ? "outline" : "soft",
|
|
249
|
+
className: "tabular-nums",
|
|
250
|
+
children: change
|
|
251
|
+
}) : null
|
|
252
|
+
]
|
|
185
253
|
}),
|
|
186
254
|
/* @__PURE__ */ jsx("span", {
|
|
187
255
|
"data-slot": "stat-card-value",
|
|
188
256
|
"data-tone": tone ?? "neutral",
|
|
189
257
|
className: cn(statCardValueVariants({ tone }), "block"),
|
|
190
|
-
children:
|
|
258
|
+
children: isLoading ? /* @__PURE__ */ jsxs(Fragment$1, { children: [placeholder("value"), /* @__PURE__ */ jsx("span", {
|
|
259
|
+
className: "sr-only",
|
|
260
|
+
children: loadingLabel
|
|
261
|
+
})] }) : figure ? /* @__PURE__ */ jsx(AnimatedCounter, {
|
|
191
262
|
value: figure.value,
|
|
192
263
|
decimals: figure.decimals,
|
|
193
264
|
prefix: figure.prefix,
|
|
@@ -198,7 +269,7 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, tone,
|
|
|
198
269
|
sub === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
199
270
|
"data-slot": "stat-card-sub",
|
|
200
271
|
className: cn(statCardSubVariants(), "block"),
|
|
201
|
-
children: typeof sub === "string" ? /* @__PURE__ */ jsx(Swap, {
|
|
272
|
+
children: isLoading ? placeholder("sub") : typeof sub === "string" ? /* @__PURE__ */ jsx(Swap, {
|
|
202
273
|
text: sub,
|
|
203
274
|
children: sub
|
|
204
275
|
}) : sub
|
|
@@ -211,7 +282,9 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, tone,
|
|
|
211
282
|
interactive: isSelected !== void 0 || onPress !== void 0 || href !== void 0
|
|
212
283
|
}), className);
|
|
213
284
|
const common = {
|
|
285
|
+
...dataAttributes(rest),
|
|
214
286
|
"data-slot": "stat-card",
|
|
287
|
+
"data-loading": isLoading || void 0,
|
|
215
288
|
"aria-label": rest["aria-label"]
|
|
216
289
|
};
|
|
217
290
|
if (isSelected !== void 0) return /* @__PURE__ */ jsx(ToggleButton, {
|
|
@@ -244,6 +317,6 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, tone,
|
|
|
244
317
|
});
|
|
245
318
|
});
|
|
246
319
|
//#endregion
|
|
247
|
-
export { StatCard, statCardHeaderVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
|
320
|
+
export { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSkeletonVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
|
248
321
|
|
|
249
322
|
//# sourceMappingURL=stat-card.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stat-card.js","names":["AriaToggleButton","AriaButton","AriaLink"],"sources":["../../src/stat-card/stat-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ReactNode, type Ref } from \"react\";\nimport {\n Button as AriaButton,\n Link as AriaLink,\n ToggleButton as AriaToggleButton,\n} from \"react-aria-components\";\n\nimport { AnimatedCounter } from \"../animated-counter/animated-counter.js\";\nimport { Badge } from \"../badge/badge.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * StatCard: one headline figure with its label and a line of context. A row\n * of them is a dashboard's summary strip.\n *\n * It exists because a summary strip is on almost every overview screen, and\n * without a shared card each screen picks its own label case, number size,\n * colour and height, so the same figure looks different on two pages.\n *\n * The figure is a counter: a number rolls up to its value the first time the\n * card scrolls into view, and when `value` changes the digits that changed roll\n * to their new faces, the way the number moved. A label, a line of context or\n * a change that is replaced rises in while the old copy lifts away. Reduced\n * motion jumps and fades instead. A value that is not a plain number\n * (a date, \"N/A\", a node) is shown as it is.\n *\n * Rules it holds:\n *\n * - The label is ONE line. A long label is cut with an ellipsis and the full\n * text is on hover, so a row of cards keeps its numbers on one baseline\n * instead of one card's label wrapping and pushing its number down.\n * - The number's colour comes from `tone`, a fixed set of meanings, never a\n * colour. `neutral` is the default and is right for any figure without a\n * verdict in it, including every zero.\n * - Cards in a row are the same height (`h-full` in a grid or flex row).\n *\n * A card can be a link (`href`, React Aria Link, so a router provider routes\n * it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a\n * React Aria ToggleButton with `aria-pressed` and one shared selected look).\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const statCardVariants = cva(\n [\n \"flex h-full min-w-0 flex-col p-6 text-start\",\n \"rounded-panel border-(length:--border-width) bg-surface text-foreground shadow-resting\",\n // box-border and font-sans are not boilerplate: the reset in\n // @seekalfred/styles is opt-in, so a consumer may load none.\n \"box-border font-sans\",\n ],\n {\n variants: {\n /** A filter card that is the one applied. */\n selected: {\n true: \"border-primary-strong\",\n false: \"border-border\",\n },\n /** Whether the card is a link or a button (hover and focus states). */\n interactive: {\n true: [\n \"cursor-pointer outline-none\",\n // A border, not a fill: a muted fill under the muted label fails contrast.\n \"data-[hovered]:not-data-[selected]:border-muted-foreground\",\n // outline-solid on the focus variant is load-bearing: outline-none\n // sets the outline style the width utility resolves from.\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n ],\n false: \"\",\n },\n },\n defaultVariants: { selected: false, interactive: false },\n },\n);\n\n/** The top row: the label on the left, the change pill on the right. */\nexport const statCardHeaderVariants = cva(\"mb-8 flex min-h-6 items-center justify-between gap-3\");\n\nexport const statCardLabelVariants = cva([\n \"m-0 min-w-0 flex-1 text-sm leading-(--leading-normal) font-normal\",\n \"text-text-secondary\",\n]);\n\nexport const statCardValueVariants = cva(\n // The counter sets its own colour; this makes it follow the tone instead.\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"m-0 font-display text-h2 leading-none font-semibold tabular-nums break-words **:data-[slot=animated-counter]:text-inherit\",\n {\n variants: {\n /** What the number MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"text-foreground\",\n positive: \"text-success-strong\",\n negative: \"text-destructive-strong\",\n warning: \"text-warning-strong\",\n attention: \"text-primary-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\nexport const statCardSubVariants = cva(\n \"m-0 mt-4 text-sm leading-(--leading-normal) text-muted-foreground\",\n);\n\nexport type StatCardTone = \"neutral\" | \"positive\" | \"negative\" | \"warning\" | \"attention\";\n\nexport interface StatCardProps {\n /** What the figure is. One line; cut with an ellipsis, full text on hover. */\n label: string;\n /**\n * The figure itself, already formatted. A plain number (`12400`), or a string\n * made of a number with text around it (`\"$12,400\"`, `\"42 days\"`, `\"98.5%\"`),\n * counts up on first view and rolls to each new value. Anything else is\n * shown as it is.\n */\n value: ReactNode;\n /** One line of context under the figure. A string that changes swaps in place. */\n sub?: ReactNode;\n /**\n * A change shown as a pill beside the label, such as `\"+12.4%\"`. A leading `+`\n * reads green and a leading `-` red; no sign is neutral. A new value swaps in\n * place and the pill follows its width.\n */\n change?: string;\n /**\n * What the figure means: `neutral` (default, and right for every zero),\n * `positive`, `negative`, `warning` (part-way or waiting) or `attention`\n * (a person has to look).\n */\n tone?: StatCardTone;\n /**\n * The card is a filter, and this says whether it is the one applied.\n * Renders a toggle button with `aria-pressed`. Pair with `onPress`.\n */\n isSelected?: boolean;\n /** Renders the card as a link to this URL. */\n href?: string;\n /** Renders the card as a button (or, with `isSelected`, a toggle). */\n onPress?: () => void;\n /** Hover text for the whole card. */\n description?: string;\n /** Anything under the context line. */\n children?: ReactNode;\n /** Layout classes for the card (margins, grid placement). */\n className?: string;\n \"aria-label\"?: string;\n}\n\n/** The number in a piece of text, for the direction a swap moves: a larger one rises from below. */\nconst amountIn = (text: string) => Number(text.replace(/,/g, \"\").match(/-?\\d+(?:\\.\\d+)?/)?.[0] ?? NaN);\n\n/** 1 when the new text holds a number that did not shrink, -1 when it did. Updated during render. */\nfunction useSwapDirection(text: string | undefined) {\n const [shown, setShown] = useState({ text, dir: 1 });\n if (shown.text !== text) {\n setShown({ text, dir: amountIn(text ?? \"\") < amountIn(shown.text ?? \"\") ? -1 : 1 });\n }\n return shown.dir;\n}\n\n/** Copy that is replaced rises in from the side it moved toward and the old copy lifts away. */\nfunction Swap({\n text,\n className,\n children,\n}: {\n text: string;\n className?: string;\n children: ReactNode;\n}) {\n const dir = useSwapDirection(text);\n return (\n <SwapText swapKey={text} dir={dir} className={cn(\"block min-w-0\", className)}>\n {children}\n </SwapText>\n );\n}\n\ninterface Figure {\n prefix: string;\n value: number;\n decimals: number;\n suffix: string;\n}\n\nconst FORMAT: Record<number, Intl.NumberFormat> = {};\nconst format = (decimals: number) =>\n (FORMAT[decimals] ??= new Intl.NumberFormat(\"en-US\", {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }));\n\n/**\n * A number with text around it, or null. Only a figure that the counter would\n * draw back as the very same text qualifies (\"12400\" would come back as\n * \"12,400\", so it is shown as written rather than quietly reformatted).\n */\nfunction figureOf(value: ReactNode): Figure | null {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null;\n const decimals = Math.min(4, (String(value).split(\".\")[1] ?? \"\").length);\n return { prefix: \"\", value, decimals, suffix: \"\" };\n }\n if (typeof value !== \"string\") return null;\n const match = /^(\\D*?)(-?\\d[\\d,]*(?:\\.\\d+)?)(\\D*)$/.exec(value);\n if (!match) return null;\n const [, prefix = \"\", digits = \"\", suffix = \"\"] = match;\n const decimals = (digits.split(\".\")[1] ?? \"\").length;\n const number = Number(digits.replace(/,/g, \"\"));\n if (!Number.isFinite(number) || `${prefix}${format(decimals).format(number)}${suffix}` !== value) {\n return null;\n }\n return { prefix, value: number, decimals, suffix };\n}\n\nconst changeTone = (change: string) =>\n /^\\+/.test(change) ? \"success\" : /^[-−]/.test(change) ? \"danger\" : \"neutral\";\n\n/**\n * One headline figure.\n *\n * @example\n * <div className=\"grid gap-4 sm:grid-cols-3\">\n * <StatCard label=\"Open\" value=\"$12,400\" sub=\"18 invoices\" change=\"+4.2%\" />\n * <StatCard label=\"Overdue\" value=\"$3,100\" sub=\"4 invoices\" tone=\"negative\" />\n * <StatCard label=\"Collected\" value=\"$9,800\" tone=\"positive\" href=\"/payments\" />\n * </div>\n */\nexport const StatCard = forwardRef<HTMLElement, StatCardProps>(function StatCard(\n { label, value, sub, change, tone, isSelected, href, onPress, description, children, className, ...rest },\n ref,\n) {\n const figure = figureOf(value);\n const body = (\n // The hover text sits on a box that fills the card: React Aria's Button\n // does not pass a `title` through to the element it renders.\n <span className=\"flex min-w-0 flex-1 flex-col\" title={description}>\n <span className={statCardHeaderVariants()}>\n <span data-slot=\"stat-card-label\" className={cn(statCardLabelVariants(), \"block\")}>\n {/* *:max-w-full: the arriving copy is an inline-block, whose shrink-to-fit width is the whole unwrapped label; capped, the label truncates. */}\n <Swap text={label} className=\"*:max-w-full\">\n <span className=\"block truncate\" title={label}>\n {label}\n </span>\n </Swap>\n </span>\n {change ? (\n <Badge\n shape=\"pill\"\n tone={changeTone(change)}\n appearance={changeTone(change) === \"neutral\" ? \"outline\" : \"soft\"}\n className=\"tabular-nums\"\n >\n {change}\n </Badge>\n ) : null}\n </span>\n <span\n data-slot=\"stat-card-value\"\n data-tone={tone ?? \"neutral\"}\n className={cn(statCardValueVariants({ tone }), \"block\")}\n >\n {figure ? (\n <AnimatedCounter\n value={figure.value}\n decimals={figure.decimals}\n prefix={figure.prefix}\n suffix={figure.suffix}\n animateOnView\n />\n ) : (\n value\n )}\n </span>\n {sub === undefined ? null : (\n <span data-slot=\"stat-card-sub\" className={cn(statCardSubVariants(), \"block\")}>\n {typeof sub === \"string\" ? <Swap text={sub}>{sub}</Swap> : sub}\n </span>\n )}\n {children}\n </span>\n );\n const interactive = isSelected !== undefined || onPress !== undefined || href !== undefined;\n const classes = cn(statCardVariants({ selected: isSelected === true, interactive }), className);\n const common = { \"data-slot\": \"stat-card\", \"aria-label\": rest[\"aria-label\"] };\n\n if (isSelected !== undefined) {\n return (\n <AriaToggleButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n isSelected={isSelected}\n onChange={() => onPress?.()}\n className={classes}\n >\n {body}\n </AriaToggleButton>\n );\n }\n if (onPress) {\n return (\n <AriaButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n onPress={onPress}\n className={classes}\n >\n {body}\n </AriaButton>\n );\n }\n if (href !== undefined) {\n return (\n <AriaLink ref={ref as Ref<HTMLAnchorElement>} {...common} href={href} className={classes}>\n {body}\n </AriaLink>\n );\n }\n return (\n <div ref={ref as Ref<HTMLDivElement>} {...common} className={classes}>\n {body}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stat-card.js","names":["AriaToggleButton","AriaButton","AriaLink"],"sources":["../../src/stat-card/stat-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useState, type ReactNode, type Ref } from \"react\";\nimport {\n Button as AriaButton,\n Link as AriaLink,\n ToggleButton as AriaToggleButton,\n} from \"react-aria-components\";\n\nimport { AnimatedCounter } from \"../animated-counter/animated-counter.js\";\nimport { Badge } from \"../badge/badge.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { skeletonVariants } from \"../skeleton/skeleton.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * StatCard: one headline figure with its label and a line of context. A row\n * of them is a dashboard's summary strip.\n *\n * It exists because a summary strip is on almost every overview screen, and\n * without a shared card each screen picks its own label case, number size,\n * colour and height, so the same figure looks different on two pages.\n *\n * The figure is a counter: a number rolls up to its value the first time the\n * card scrolls into view, and when `value` changes the digits that changed roll\n * to their new faces, the way the number moved. A label, a line of context or\n * a change that is replaced rises in while the old copy lifts away. Reduced\n * motion jumps and fades instead. A value that is not a plain number\n * (a date, \"N/A\", a node) is shown as it is.\n *\n * Rules it holds:\n *\n * - The label is ONE line. A long label is cut with an ellipsis and the full\n * text is on hover, so a row of cards keeps its numbers on one baseline\n * instead of one card's label wrapping and pushing its number down.\n * - The number's colour comes from `tone`, a fixed set of meanings, never a\n * colour. `neutral` is the default and is right for any figure without a\n * verdict in it, including every zero.\n * - Cards in a row are the same height (`h-full` in a grid or flex row).\n * - The change pill's colour is what the change MEANS. By default a rise is\n * good; `trend=\"inverse\"` reads a rise as bad (bounce rate, cost, keywords\n * that dropped out), `trend=\"neutral\"` gives no verdict, and `changeTone`\n * names the meaning outright when the text carries no sign.\n *\n * A card can be a link (`href`, React Aria Link, so a router provider routes\n * it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a\n * React Aria ToggleButton with `aria-pressed` and one shared selected look).\n *\n * `isLoading` keeps the card's box while its figure is fetched: the label\n * stays (or pulses as a placeholder when it is empty), the figure and the\n * context line become placeholder blocks the height of the lines they stand\n * in for, and the change pill pulses at its own size. So a row of loading cards is exactly as\n * tall as the row that replaces it and nothing shifts when the data lands.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const statCardVariants = cva(\n [\n \"flex h-full min-w-0 flex-col p-6 text-start\",\n \"rounded-panel border-(length:--border-width) bg-surface text-foreground shadow-resting\",\n // box-border and font-sans are not boilerplate: the reset in\n // @seekalfred/styles is opt-in, so a consumer may load none.\n \"box-border font-sans\",\n ],\n {\n variants: {\n /** A filter card that is the one applied. */\n selected: {\n true: \"border-primary-strong\",\n false: \"border-border\",\n },\n /** Whether the card is a link or a button (hover and focus states). */\n interactive: {\n true: [\n \"cursor-pointer outline-none\",\n // A border, not a fill: a muted fill under the muted label fails contrast.\n \"data-[hovered]:not-data-[selected]:border-muted-foreground\",\n // outline-solid on the focus variant is load-bearing: outline-none\n // sets the outline style the width utility resolves from.\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n ],\n false: \"\",\n },\n },\n defaultVariants: { selected: false, interactive: false },\n },\n);\n\n/** The top row: the label on the left, the change pill on the right. */\nexport const statCardHeaderVariants = cva(\"mb-8 flex min-h-6 items-center justify-between gap-3\");\n\nexport const statCardLabelVariants = cva([\n \"m-0 min-w-0 flex-1 text-sm leading-(--leading-normal) font-normal\",\n \"text-text-secondary\",\n]);\n\nexport const statCardValueVariants = cva(\n // The counter sets its own colour; this makes it follow the tone instead.\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"m-0 font-display text-h2 leading-none font-semibold tabular-nums break-words **:data-[slot=animated-counter]:text-inherit\",\n {\n variants: {\n /** What the number MEANS. Colour follows from it; it is never picked directly. */\n tone: {\n neutral: \"text-foreground\",\n positive: \"text-success-strong\",\n negative: \"text-destructive-strong\",\n warning: \"text-warning-strong\",\n attention: \"text-primary-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/** An optional icon before the label. Decorative: the label names the figure. */\nexport const statCardIconVariants = cva(\n \"flex shrink-0 items-center text-muted-foreground [&_svg]:size-4 [&_svg]:shrink-0\",\n);\n\nexport const statCardSubVariants = cva(\n \"m-0 mt-4 text-sm leading-(--leading-normal) text-muted-foreground\",\n);\n\n/**\n * A placeholder standing in for one line of copy while the card loads. It sits\n * inside the line it replaces and is one line-height tall (`1lh`), so the line\n * box, and with it the card, keeps the height of the loaded copy.\n */\nexport const statCardSkeletonVariants = cva(\"inline-block h-[1lh] max-w-full align-top\", {\n variants: {\n /** Which line it stands in for: each has the rough width of its copy. */\n part: {\n label: \"w-24\",\n value: \"w-28\",\n sub: \"w-36\",\n },\n },\n});\n\nexport type StatCardTone = \"neutral\" | \"positive\" | \"negative\" | \"warning\" | \"attention\";\n\n/** Which way is good for this figure: up (`normal`), down (`inverse`), or neither (`neutral`). */\nexport type StatCardTrend = \"normal\" | \"inverse\" | \"neutral\";\n\n/** What a change means, whatever its sign. */\nexport type StatCardChangeTone = \"positive\" | \"negative\" | \"neutral\";\n\nexport interface StatCardProps {\n /** What the figure is. One line; cut with an ellipsis, full text on hover. */\n label: string;\n /**\n * The figure itself, already formatted. A plain number (`12400`), or a string\n * made of a number with text around it (`\"$12,400\"`, `\"42 days\"`, `\"98.5%\"`),\n * counts up on first view and rolls to each new value. Anything else is\n * shown as it is.\n */\n value: ReactNode;\n /** One line of context under the figure. A string that changes swaps in place. */\n sub?: ReactNode;\n /**\n * A change shown as a pill beside the label, such as `\"+12.4%\"`. A leading `+`\n * reads green and a leading `-` red; no sign is neutral. A new value swaps in\n * place and the pill follows its width.\n */\n change?: string;\n /**\n * Which direction of `change` is good. `normal` (default): a leading `+`\n * reads green and a leading `-` red. `inverse`: a rise reads red and a fall\n * green, for a figure where more is worse (bounce rate, cost per click,\n * keywords that fell out of the top 100). `neutral`: no colour either way.\n * @default \"normal\"\n */\n trend?: StatCardTrend;\n /**\n * The meaning of `change`, named outright. Wins over `trend` and the sign;\n * use it when the text has no `+` or `-` (`\"▲4\"`) or the verdict comes from\n * elsewhere. It colours only the change pill, never the value (that is\n * `tone`).\n */\n changeTone?: StatCardChangeTone;\n /**\n * An icon before the label. Decorative (hidden from assistive technology).\n * An SVG is drawn at 16px, whatever size it is given. A falsy value\n * (`false`, `null`, `\"\"`) renders nothing, so `icon={cond && <Icon />}` works.\n */\n icon?: ReactNode;\n /**\n * What the figure means: `neutral` (default, and right for every zero),\n * `positive`, `negative`, `warning` (part-way or waiting) or `attention`\n * (a person has to look).\n */\n tone?: StatCardTone;\n /**\n * The card is a filter, and this says whether it is the one applied.\n * Renders a toggle button with `aria-pressed`. Pair with `onPress`.\n */\n isSelected?: boolean;\n /** Renders the card as a link to this URL. */\n href?: string;\n /** Renders the card as a button (or, with `isSelected`, a toggle). */\n onPress?: () => void;\n /** Hover text for the whole card. */\n description?: string;\n /**\n * The figure is still loading. The card keeps its box: the label stays (a\n * placeholder pulses in its place when it is `\"\"`), the value and, when\n * `sub` is given, the context line become placeholder blocks the height of\n * those lines, and the change pill becomes a placeholder of its size. The card's content carries\n * `aria-busy`, the root `data-loading`, and assistive technology reads\n * `loadingLabel` in place of the figure. A link, button or toggle card stays\n * operable. Pass `sub` (any value, even `\"\"`) while loading to keep the\n * context line's height.\n * @default false\n */\n isLoading?: boolean;\n /**\n * Read by assistive technology in place of the figure while `isLoading`.\n * @default \"Loading\"\n */\n loadingLabel?: string;\n /** Anything under the context line. */\n children?: ReactNode;\n /** Layout classes for the card (margins, grid placement). */\n className?: string;\n \"aria-label\"?: string;\n /** `data-*` attributes land on the card's root (test ids, analytics hooks). `data-slot` stays the card's own. */\n [dataAttribute: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** The number in a piece of text, for the direction a swap moves: a larger one rises from below. */\nconst amountIn = (text: string) => Number(text.replace(/,/g, \"\").match(/-?\\d+(?:\\.\\d+)?/)?.[0] ?? NaN);\n\n/** 1 when the new text holds a number that did not shrink, -1 when it did. Updated during render. */\nfunction useSwapDirection(text: string | undefined) {\n const [shown, setShown] = useState({ text, dir: 1 });\n if (shown.text !== text) {\n setShown({ text, dir: amountIn(text ?? \"\") < amountIn(shown.text ?? \"\") ? -1 : 1 });\n }\n return shown.dir;\n}\n\n/** Copy that is replaced rises in from the side it moved toward and the old copy lifts away. */\nfunction Swap({\n text,\n className,\n children,\n}: {\n text: string;\n className?: string;\n children: ReactNode;\n}) {\n const dir = useSwapDirection(text);\n return (\n <SwapText swapKey={text} dir={dir} className={cn(\"block min-w-0\", className)}>\n {children}\n </SwapText>\n );\n}\n\ninterface Figure {\n prefix: string;\n value: number;\n decimals: number;\n suffix: string;\n}\n\nconst FORMAT: Record<number, Intl.NumberFormat> = {};\nconst format = (decimals: number) =>\n (FORMAT[decimals] ??= new Intl.NumberFormat(\"en-US\", {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }));\n\n/**\n * A number with text around it, or null. Only a figure that the counter would\n * draw back as the very same text qualifies (\"12400\" would come back as\n * \"12,400\", so it is shown as written rather than quietly reformatted).\n */\nfunction figureOf(value: ReactNode): Figure | null {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null;\n const decimals = Math.min(4, (String(value).split(\".\")[1] ?? \"\").length);\n return { prefix: \"\", value, decimals, suffix: \"\" };\n }\n if (typeof value !== \"string\") return null;\n const match = /^(\\D*?)(-?\\d[\\d,]*(?:\\.\\d+)?)(\\D*)$/.exec(value);\n if (!match) return null;\n const [, prefix = \"\", digits = \"\", suffix = \"\"] = match;\n const decimals = (digits.split(\".\")[1] ?? \"\").length;\n const number = Number(digits.replace(/,/g, \"\"));\n if (!Number.isFinite(number) || `${prefix}${format(decimals).format(number)}${suffix}` !== value) {\n return null;\n }\n return { prefix, value: number, decimals, suffix };\n}\n\nconst signOf = (change: string) => (/^\\+/.test(change) ? 1 : /^[-−]/.test(change) ? -1 : 0);\n\n/** What a change means: the explicit tone, else its sign read the way `trend` says. */\nfunction changeMeaning(\n change: string,\n trend: StatCardTrend,\n explicit: StatCardChangeTone | undefined,\n): StatCardChangeTone {\n if (explicit) return explicit;\n const sign = trend === \"neutral\" ? 0 : signOf(change) * (trend === \"inverse\" ? -1 : 1);\n return sign > 0 ? \"positive\" : sign < 0 ? \"negative\" : \"neutral\";\n}\n\nconst BADGE_TONE = { positive: \"success\", negative: \"danger\", neutral: \"neutral\" } as const;\n\n/** The swap key of an empty label's placeholder: a key no label text can be. */\nconst LOADING_KEY = \"\\u0000loading\";\n\n/** Only the `data-*` attributes, so nothing else in the rest object reaches the DOM. */\nconst dataAttributes = (rest: Record<string, unknown>) =>\n Object.fromEntries(Object.entries(rest).filter(([key]) => key.startsWith(\"data-\")));\n\n/**\n * One headline figure.\n *\n * @example\n * <div className=\"grid gap-4 sm:grid-cols-3\">\n * <StatCard label=\"Open\" value=\"$12,400\" sub=\"18 invoices\" change=\"+4.2%\" />\n * <StatCard label=\"Overdue\" value=\"$3,100\" sub=\"4 invoices\" tone=\"negative\" />\n * <StatCard label=\"Collected\" value=\"$9,800\" tone=\"positive\" href=\"/payments\" />\n * </div>\n */\nexport const StatCard = forwardRef<HTMLElement, StatCardProps>(function StatCard(\n {\n label,\n value,\n sub,\n change,\n trend = \"normal\",\n changeTone,\n icon,\n tone,\n isSelected,\n href,\n onPress,\n description,\n isLoading = false,\n loadingLabel = \"Loading\",\n children,\n className,\n ...rest\n },\n ref,\n) {\n const figure = figureOf(value);\n const placeholder = (part: \"label\" | \"value\" | \"sub\") => (\n <span\n data-slot=\"stat-card-skeleton\"\n aria-hidden=\"true\"\n className={cnAll(skeletonVariants(), statCardSkeletonVariants({ part }))}\n />\n );\n const meaning = change ? changeMeaning(change, trend, changeTone) : \"neutral\";\n const body = (\n // The hover text sits on a box that fills the card: React Aria's Button\n // does not pass a `title` through to the element it renders.\n // aria-busy sits here, on the content being replaced, because React Aria's Button, ToggleButton and Link drop it from their props.\n <span\n data-slot=\"stat-card-body\"\n aria-busy={isLoading || undefined}\n className=\"flex min-w-0 flex-1 flex-col\"\n title={description}\n >\n <span className={statCardHeaderVariants()}>\n {icon ? (\n <span data-slot=\"stat-card-icon\" aria-hidden=\"true\" className={statCardIconVariants()}>\n {icon}\n </span>\n ) : null}\n <span data-slot=\"stat-card-label\" className={cn(statCardLabelVariants(), \"block\")}>\n {/* *:max-w-full: the arriving copy is an inline-block, whose shrink-to-fit width is the whole unwrapped label; capped, the label truncates. */}\n {/* The placeholder sits in the label's own line (a swap with an empty key renders nothing), so the header keeps its height. */}\n <Swap text={isLoading && label === \"\" ? LOADING_KEY : label} className=\"*:max-w-full\">\n <span className=\"block truncate\" title={label}>\n {isLoading && label === \"\" ? placeholder(\"label\") : label}\n </span>\n </Swap>\n </span>\n {change && isLoading ? (\n // The pill's own box, invisible inside a pulsing placeholder, so the header keeps its height.\n <span\n data-slot=\"stat-card-skeleton\"\n aria-hidden=\"true\"\n // Skeleton's surface and pulse, in the pill's shape (tailwind-merge does not know rounded-pill replaces rounded-md).\n className=\"box-border inline-flex shrink-0 rounded-pill bg-muted motion-safe:animate-skeleton-pulse\"\n >\n <Badge\n shape=\"pill\"\n tone=\"neutral\"\n appearance=\"outline\"\n className=\"invisible tabular-nums\"\n >\n {change}\n </Badge>\n </span>\n ) : change ? (\n <Badge\n shape=\"pill\"\n tone={BADGE_TONE[meaning]}\n appearance={meaning === \"neutral\" ? \"outline\" : \"soft\"}\n className=\"tabular-nums\"\n >\n {change}\n </Badge>\n ) : null}\n </span>\n <span\n data-slot=\"stat-card-value\"\n data-tone={tone ?? \"neutral\"}\n className={cn(statCardValueVariants({ tone }), \"block\")}\n >\n {isLoading ? (\n <>\n {placeholder(\"value\")}\n <span className=\"sr-only\">{loadingLabel}</span>\n </>\n ) : figure ? (\n <AnimatedCounter\n value={figure.value}\n decimals={figure.decimals}\n prefix={figure.prefix}\n suffix={figure.suffix}\n animateOnView\n />\n ) : (\n value\n )}\n </span>\n {sub === undefined ? null : (\n <span data-slot=\"stat-card-sub\" className={cn(statCardSubVariants(), \"block\")}>\n {isLoading ? (\n placeholder(\"sub\")\n ) : typeof sub === \"string\" ? (\n <Swap text={sub}>{sub}</Swap>\n ) : (\n sub\n )}\n </span>\n )}\n {children}\n </span>\n );\n const interactive = isSelected !== undefined || onPress !== undefined || href !== undefined;\n const classes = cn(statCardVariants({ selected: isSelected === true, interactive }), className);\n const common = {\n ...dataAttributes(rest),\n \"data-slot\": \"stat-card\",\n \"data-loading\": isLoading || undefined,\n \"aria-label\": rest[\"aria-label\"],\n };\n\n if (isSelected !== undefined) {\n return (\n <AriaToggleButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n isSelected={isSelected}\n onChange={() => onPress?.()}\n className={classes}\n >\n {body}\n </AriaToggleButton>\n );\n }\n if (onPress) {\n return (\n <AriaButton\n ref={ref as Ref<HTMLButtonElement>}\n {...common}\n onPress={onPress}\n className={classes}\n >\n {body}\n </AriaButton>\n );\n }\n if (href !== undefined) {\n return (\n <AriaLink ref={ref as Ref<HTMLAnchorElement>} {...common} href={href} className={classes}>\n {body}\n </AriaLink>\n );\n }\n return (\n <div ref={ref as Ref<HTMLDivElement>} {...common} className={classes}>\n {body}\n </div>\n );\n});\n"],"mappings":"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"}
|