@seekalfred/primitives 0.7.3 → 0.8.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/README.md +315 -4
- package/dist/alert/alert.d.ts +2 -2
- package/dist/alert/alert.js +1 -1
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +14 -0
- package/dist/alert-dialog/alert-dialog.js +52 -27
- 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 +16 -1
- package/dist/badge/badge.js +12 -5
- package/dist/badge/badge.js.map +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 +24 -2
- package/dist/button/button.js +62 -13
- package/dist/button/button.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +25 -1
- package/dist/checkbox/checkbox.js +103 -28
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/checkbox/index.d.ts +2 -2
- package/dist/checkbox/index.js +2 -2
- 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/code-block/code-block.d.ts +45 -0
- package/dist/code-block/code-block.js +74 -0
- package/dist/code-block/code-block.js.map +1 -0
- package/dist/code-block/index.d.ts +2 -0
- package/dist/code-block/index.js +2 -0
- package/dist/command-palette/command-palette.d.ts +128 -0
- package/dist/command-palette/command-palette.js +371 -0
- package/dist/command-palette/command-palette.js.map +1 -0
- package/dist/command-palette/index.d.ts +2 -0
- package/dist/command-palette/index.js +2 -0
- package/dist/copy-button/copy-button.d.ts +19 -3
- package/dist/copy-button/copy-button.js +7 -4
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/empty-state/empty-state.d.ts +17 -0
- package/dist/empty-state/empty-state.js +9 -7
- package/dist/empty-state/empty-state.js.map +1 -1
- 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/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 +14 -9
- package/dist/index.js +14 -10
- package/dist/info-tooltip/index.d.ts +2 -0
- package/dist/info-tooltip/index.js +2 -0
- package/dist/info-tooltip/info-tooltip.d.ts +35 -0
- package/dist/info-tooltip/info-tooltip.js +49 -0
- package/dist/info-tooltip/info-tooltip.js.map +1 -0
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +4 -2
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +2 -6
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/lib/selection-control.js +32 -1
- package/dist/lib/selection-control.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +106 -3
- package/dist/line-chart/line-chart.js +185 -46
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/mention-input/mention-input.js +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +14 -0
- package/dist/money-input/money-input.js +17 -7
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +8 -1
- package/dist/morph-select/morph-select.js +8 -5
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/multi-select.d.ts +31 -0
- package/dist/multi-select/multi-select.js +123 -6
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/number-field/number-field.js +1 -1
- package/dist/password-field/password-field.js +1 -1
- package/dist/password-strength/password-strength.js +1 -1
- package/dist/phone-input/phone-input.js +1 -1
- package/dist/popover/popover.js +1 -1
- 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 +53 -2
- package/dist/progress/progress.js +146 -36
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.d.ts +12 -3
- package/dist/radio-group/radio-group.js +62 -52
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/scroll-area/scroll-area.d.ts +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.js +1 -1
- package/dist/select/select.d.ts +10 -1
- package/dist/select/select.js +4 -3
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/shortcut-recorder/shortcut-recorder.js +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 +32 -1
- package/dist/stat-card/stat-card.js +52 -9
- 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 +211 -3
- package/dist/table/table.js +473 -93
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.d.ts +4 -2
- package/dist/tabs/tabs.js +7 -5
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/tag-input.js +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 +4 -2
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/toggle-button/index.d.ts +2 -0
- package/dist/toggle-button/index.js +2 -0
- package/dist/toggle-button/toggle-button.d.ts +93 -0
- package/dist/toggle-button/toggle-button.js +199 -0
- package/dist/toggle-button/toggle-button.js.map +1 -0
- package/package.json +19 -3
|
@@ -16,14 +16,23 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
16
16
|
* keys scrub through the points: a cursor rides the line, the stretch after it
|
|
17
17
|
* dims, and the headline and context show that point.
|
|
18
18
|
*
|
|
19
|
+
* A missing point (`null`, `undefined` or not a finite number) is a gap: the
|
|
20
|
+
* line breaks around it and the scale ignores it. A point with missing
|
|
21
|
+
* neighbours on both sides is drawn as a dot.
|
|
22
|
+
*
|
|
19
23
|
* Plain SVG, plain CSS transitions and one small spring tween for the two
|
|
20
24
|
* things CSS cannot animate (the shape, and the cursor riding it). No chart or
|
|
21
25
|
* animation library.
|
|
22
26
|
*/
|
|
23
27
|
const sparklineVariants = cva("grid min-w-0 gap-3 m-0 box-border font-sans text-foreground");
|
|
24
28
|
/** Space kept above the highest and below the lowest point, and half the line width. */
|
|
25
|
-
const INSET =
|
|
29
|
+
const INSET = {
|
|
30
|
+
md: 5,
|
|
31
|
+
sm: 3
|
|
32
|
+
};
|
|
26
33
|
const CAP = 1.25;
|
|
34
|
+
/** Radius of a lone point's dot, about the line's width. */
|
|
35
|
+
const DOT = 2.5;
|
|
27
36
|
const GRID = Array.from({ length: 161 }, (_, index) => index / 160);
|
|
28
37
|
const GROUPED = new Intl.NumberFormat("en-US");
|
|
29
38
|
/** The first draw: one stroke, slow at both ends like a pen (480ms x 1.75). */
|
|
@@ -53,6 +62,31 @@ function curveFor(data) {
|
|
|
53
62
|
points.push([1, ys[last]]);
|
|
54
63
|
return points;
|
|
55
64
|
}
|
|
65
|
+
/** Fills each missing point straight between its present neighbours (or with the nearest one at the ends), so a gapped line still has a shape to draw and morph. */
|
|
66
|
+
function fillGaps(values) {
|
|
67
|
+
const present = values.flatMap((value, index) => value === null ? [] : [index]);
|
|
68
|
+
if (!present.length) return values.map(() => 0);
|
|
69
|
+
return values.map((value, index) => {
|
|
70
|
+
if (value !== null) return value;
|
|
71
|
+
const after = present.find((at) => at > index);
|
|
72
|
+
const before = present.findLast((at) => at < index);
|
|
73
|
+
if (before === void 0) return values[after];
|
|
74
|
+
if (after === void 0) return values[before];
|
|
75
|
+
return values[before] + (values[after] - values[before]) * (index - before) / (after - before);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** The stretches of x (0 to 1) where the line is drawn: from each present point to the next, split where missing points sit between. */
|
|
79
|
+
function runsFor(values) {
|
|
80
|
+
const last = values.length - 1;
|
|
81
|
+
const runs = [];
|
|
82
|
+
values.forEach((value, index) => {
|
|
83
|
+
if (value === null) return;
|
|
84
|
+
const run = runs[runs.length - 1];
|
|
85
|
+
if (run && values[index - 1] !== null && index > 0) run[1] = index / last;
|
|
86
|
+
else runs.push([index / last, index / last]);
|
|
87
|
+
});
|
|
88
|
+
return runs;
|
|
89
|
+
}
|
|
56
90
|
/** Height of the sampled line at x, straight between samples. */
|
|
57
91
|
function heightAt(shape, x) {
|
|
58
92
|
let low = 0;
|
|
@@ -69,8 +103,8 @@ function heightAt(shape, x) {
|
|
|
69
103
|
return x1 === x0 ? y1 : y0 + (y1 - y0) * (x - x0) / (x1 - x0);
|
|
70
104
|
}
|
|
71
105
|
/** Every drawn string for one frame, in the chart's own pixels so strokes and dots stay round at any width. */
|
|
72
|
-
function geometryOf(shape, w, h, at) {
|
|
73
|
-
const toY = (y) =>
|
|
106
|
+
function geometryOf(shape, w, h, at, inset) {
|
|
107
|
+
const toY = (y) => inset + (1 - y) * (h - inset * 2);
|
|
74
108
|
const pairs = shape.map(([x, y]) => `${(x * w).toFixed(2)},${toY(y).toFixed(2)}`);
|
|
75
109
|
const cx = at * w;
|
|
76
110
|
const cy = toY(heightAt(shape, at));
|
|
@@ -88,11 +122,15 @@ const TONE = {
|
|
|
88
122
|
warning: "text-warning-strong [--spark-stroke:var(--color-warning-strong)]",
|
|
89
123
|
danger: "text-destructive-strong [--spark-stroke:var(--color-destructive-strong)]"
|
|
90
124
|
};
|
|
91
|
-
const Sparkline = forwardRef(function Sparkline({ data, label, value, change, tone = "accent", width = 160, height = 52, labels, formatValue, area = true, interactive = true, className }, ref) {
|
|
92
|
-
const
|
|
125
|
+
const Sparkline = forwardRef(function Sparkline({ data, label, value, change, tone = "accent", color, captionHidden = false, size: scale = "md", missingLabel = "No data", width = 160, height = 52, labels, formatValue, area = true, interactive = true, className }, ref) {
|
|
126
|
+
const points = data.map((point) => typeof point === "number" && Number.isFinite(point) ? point : null);
|
|
127
|
+
const values = points.length > 1 ? points : [points[0] ?? null, points[0] ?? null];
|
|
128
|
+
const series = fillGaps(values);
|
|
93
129
|
const last = series.length - 1;
|
|
94
130
|
const key = series.join(",");
|
|
95
131
|
const target = useMemo(() => curveFor(key.split(",").map(Number)), [key]);
|
|
132
|
+
const gapped = values.includes(null);
|
|
133
|
+
const inset = INSET[scale];
|
|
96
134
|
const uid = `spark${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
97
135
|
const figure = useRef(null);
|
|
98
136
|
const refs = useMemo(() => mergeRefs(ref, figure), [ref]);
|
|
@@ -102,7 +140,8 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
102
140
|
const cursorAt = useRef(1);
|
|
103
141
|
const box = useRef({
|
|
104
142
|
w: width,
|
|
105
|
-
h: height
|
|
143
|
+
h: height,
|
|
144
|
+
inset
|
|
106
145
|
});
|
|
107
146
|
const [size, setSize] = useState({
|
|
108
147
|
w: width,
|
|
@@ -120,18 +159,22 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
120
159
|
index,
|
|
121
160
|
dir: index === null || moved.index === null ? 1 : Math.sign(index - moved.index) || 1
|
|
122
161
|
});
|
|
123
|
-
const [first] = useState(() => geometryOf(target, width, height, 1));
|
|
162
|
+
const [first] = useState(() => geometryOf(target, width, height, 1, inset));
|
|
124
163
|
/** One writer for every drawn attribute, straight onto the elements: no re-render per frame. */
|
|
125
164
|
const paint = useCallback(() => {
|
|
126
165
|
const root = svg.current;
|
|
127
166
|
if (!root) return;
|
|
128
|
-
const { w, h } = box.current;
|
|
129
|
-
const g = geometryOf(shape.current, w, h, cursorAt.current);
|
|
167
|
+
const { w, h, inset: room } = box.current;
|
|
168
|
+
const g = geometryOf(shape.current, w, h, cursorAt.current, room);
|
|
130
169
|
const q = (name) => root.querySelector(`[data-part="${name}"]`);
|
|
131
170
|
q("line")?.setAttribute("points", g.line);
|
|
132
171
|
q("trail")?.setAttribute("d", g.trail);
|
|
133
172
|
q("area")?.setAttribute("d", g.fill);
|
|
134
173
|
q("dot")?.setAttribute("points", g.dot);
|
|
174
|
+
for (const node of root.querySelectorAll("[data-part=\"point\"]")) {
|
|
175
|
+
const y = heightAt(shape.current, Number(node.dataset.at));
|
|
176
|
+
node.setAttribute("cy", String(room + (1 - y) * (h - room * 2)));
|
|
177
|
+
}
|
|
135
178
|
q("lit")?.setAttribute("width", String(g.cx + CAP + 16));
|
|
136
179
|
q("dim")?.setAttribute("x", String(g.cx + CAP));
|
|
137
180
|
q("cursor")?.setAttribute("x1", String(g.cx));
|
|
@@ -144,6 +187,7 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
144
187
|
const w = node.clientWidth;
|
|
145
188
|
if (!w || Math.abs(box.current.w - w) < .5) return;
|
|
146
189
|
box.current = {
|
|
190
|
+
...box.current,
|
|
147
191
|
w,
|
|
148
192
|
h: w * height / width
|
|
149
193
|
};
|
|
@@ -152,7 +196,14 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
152
196
|
observer.observe(node);
|
|
153
197
|
return () => observer.disconnect();
|
|
154
198
|
}, [height, width]);
|
|
155
|
-
useIsomorphicLayoutEffect(
|
|
199
|
+
useIsomorphicLayoutEffect(() => {
|
|
200
|
+
box.current.inset = inset;
|
|
201
|
+
paint();
|
|
202
|
+
}, [
|
|
203
|
+
size,
|
|
204
|
+
inset,
|
|
205
|
+
paint
|
|
206
|
+
]);
|
|
156
207
|
useEffect(() => {
|
|
157
208
|
const node = figure.current;
|
|
158
209
|
if (!node) return;
|
|
@@ -202,12 +253,20 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
202
253
|
paint
|
|
203
254
|
]);
|
|
204
255
|
const scrubbing = index !== null;
|
|
205
|
-
const format = (at) => formatValue ? formatValue(
|
|
256
|
+
const format = (at) => values[at] === null ? missingLabel : formatValue ? formatValue(values[at], at) : GROUPED.format(values[at]);
|
|
206
257
|
const point = format(index ?? last);
|
|
207
258
|
const when = labels?.[index ?? last];
|
|
208
259
|
const headline = value === void 0 ? void 0 : scrubbing ? point : value;
|
|
209
260
|
const aside = index === null ? change ?? "" : value === void 0 ? [point, when].filter(Boolean).join(" · ") : when ?? `${index + 1} of ${last + 1}`;
|
|
210
|
-
const description = `${label}: ${value ??
|
|
261
|
+
const description = `${label}: ${value ?? values[last] ?? missingLabel}. ${change ?? ""}`.trim();
|
|
262
|
+
const runs = gapped ? runsFor(values) : [];
|
|
263
|
+
const gaps = gapped ? `url(#${uid}-gaps)` : void 0;
|
|
264
|
+
const toY = (y) => inset + (1 - y) * (size.h - inset * 2);
|
|
265
|
+
const lone = runs.filter(([from, to]) => from === to).map(([at]) => ({
|
|
266
|
+
at,
|
|
267
|
+
cx: at * size.w,
|
|
268
|
+
cy: toY(heightAt(shape.current, at))
|
|
269
|
+
}));
|
|
211
270
|
const pointAt = (clientX) => {
|
|
212
271
|
const rect = plot.current?.getBoundingClientRect();
|
|
213
272
|
return rect?.width ? Math.max(0, Math.min(last, Math.round((clientX - rect.left) / rect.width * last))) : last;
|
|
@@ -260,9 +319,10 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
260
319
|
"data-drawn": drawn || void 0,
|
|
261
320
|
"data-landed": landed || void 0,
|
|
262
321
|
"data-scrubbing": scrubbing || void 0,
|
|
322
|
+
"data-size": scale === "md" ? void 0 : scale,
|
|
263
323
|
"aria-label": description,
|
|
264
324
|
className: cn(sparklineVariants(), className),
|
|
265
|
-
children: [/* @__PURE__ */ jsxs("figcaption", {
|
|
325
|
+
children: [captionHidden ? null : /* @__PURE__ */ jsxs("figcaption", {
|
|
266
326
|
className: "flex min-w-0 items-baseline gap-2 text-sm text-text-secondary",
|
|
267
327
|
children: [
|
|
268
328
|
/* @__PURE__ */ jsx("span", {
|
|
@@ -303,6 +363,10 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
303
363
|
"aria-hidden": "true",
|
|
304
364
|
focusable: "false",
|
|
305
365
|
className: cnAll("block h-auto w-full overflow-visible", TONE[tone]),
|
|
366
|
+
style: color ? {
|
|
367
|
+
color,
|
|
368
|
+
"--spark-stroke": color
|
|
369
|
+
} : void 0,
|
|
306
370
|
children: [
|
|
307
371
|
/* @__PURE__ */ jsxs("defs", { children: [
|
|
308
372
|
/* @__PURE__ */ jsx("clipPath", {
|
|
@@ -338,13 +402,35 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
338
402
|
height: size.h + 32,
|
|
339
403
|
style: { width: drawn ? size.w + 32 : 0 }
|
|
340
404
|
})
|
|
341
|
-
})
|
|
405
|
+
}),
|
|
406
|
+
gapped ? /* @__PURE__ */ jsx("mask", {
|
|
407
|
+
id: `${uid}-gaps`,
|
|
408
|
+
maskUnits: "userSpaceOnUse",
|
|
409
|
+
x: -16,
|
|
410
|
+
y: -16,
|
|
411
|
+
width: size.w + 32,
|
|
412
|
+
height: size.h + 32,
|
|
413
|
+
children: runs.map(([from, to]) => {
|
|
414
|
+
const reach = from === to ? DOT : CAP;
|
|
415
|
+
const x = from === 0 ? -16 : from * size.w - reach;
|
|
416
|
+
const end = to === 1 ? size.w + 16 : to * size.w + reach;
|
|
417
|
+
return /* @__PURE__ */ jsx("rect", {
|
|
418
|
+
"data-part": "run",
|
|
419
|
+
x,
|
|
420
|
+
y: -16,
|
|
421
|
+
width: end - x,
|
|
422
|
+
height: size.h + 32,
|
|
423
|
+
fill: "white"
|
|
424
|
+
}, from);
|
|
425
|
+
})
|
|
426
|
+
}) : null
|
|
342
427
|
] }),
|
|
343
428
|
area ? /* @__PURE__ */ jsx("path", {
|
|
344
429
|
"data-part": "area",
|
|
345
430
|
className: "sparkline-area",
|
|
346
431
|
d: first.fill,
|
|
347
|
-
clipPath: `url(#${uid}-fill)
|
|
432
|
+
clipPath: `url(#${uid}-fill)`,
|
|
433
|
+
mask: gaps
|
|
348
434
|
}) : null,
|
|
349
435
|
/* @__PURE__ */ jsx("line", {
|
|
350
436
|
className: "sparkline-baseline",
|
|
@@ -357,15 +443,25 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
357
443
|
"data-part": "trail",
|
|
358
444
|
className: "sparkline-trail",
|
|
359
445
|
d: first.trail,
|
|
360
|
-
clipPath: `url(#${uid}-dim)
|
|
446
|
+
clipPath: `url(#${uid}-dim)`,
|
|
447
|
+
mask: gaps
|
|
361
448
|
}),
|
|
362
449
|
/* @__PURE__ */ jsx("polyline", {
|
|
363
450
|
"data-part": "line",
|
|
364
451
|
className: "sparkline-line",
|
|
365
452
|
points: first.line,
|
|
366
453
|
pathLength: 1,
|
|
367
|
-
clipPath: `url(#${uid}-lit)
|
|
454
|
+
clipPath: `url(#${uid}-lit)`,
|
|
455
|
+
mask: gaps
|
|
368
456
|
}),
|
|
457
|
+
lone.map(({ at, cx, cy }) => /* @__PURE__ */ jsx("circle", {
|
|
458
|
+
"data-part": "point",
|
|
459
|
+
"data-at": at,
|
|
460
|
+
className: "sparkline-point",
|
|
461
|
+
cx,
|
|
462
|
+
cy,
|
|
463
|
+
r: DOT
|
|
464
|
+
}, cx)),
|
|
369
465
|
/* @__PURE__ */ jsx("line", {
|
|
370
466
|
"data-part": "cursor",
|
|
371
467
|
className: "sparkline-cursor",
|
|
@@ -377,7 +473,8 @@ const Sparkline = forwardRef(function Sparkline({ data, label, value, change, to
|
|
|
377
473
|
/* @__PURE__ */ jsx("polyline", {
|
|
378
474
|
"data-part": "dot",
|
|
379
475
|
className: "sparkline-dot",
|
|
380
|
-
points: first.dot
|
|
476
|
+
points: first.dot,
|
|
477
|
+
mask: gaps
|
|
381
478
|
})
|
|
382
479
|
]
|
|
383
480
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sparkline.js","names":[],"sources":["../../src/sparkline/sparkline.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type PointerEvent,\n} from \"react\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { prefersReducedMotion, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { SMOOTH, SNAPPY, tween } from \"../lib/spring-tween.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Sparkline: a small trend line with its label, an optional headline value and\n * a line of context (the change). The line draws itself left to right the first\n * time it is seen, the fill follows the pen, and the end dot lands. New data\n * morphs the line from the shape on screen. Hovering, dragging or the arrow\n * keys scrub through the points: a cursor rides the line, the stretch after it\n * dims, and the headline and context show that point.\n *\n * Plain SVG, plain CSS transitions and one small spring tween for the two\n * things CSS cannot animate (the shape, and the cursor riding it). No chart or\n * animation library.\n */\nexport const sparklineVariants = cva(\"grid min-w-0 gap-3 m-0 box-border font-sans text-foreground\");\n\nexport type SparklineTone = \"accent\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface SparklineProps {\n /** The series, oldest first. One value draws a flat line. */\n data: number[];\n /** What the line is. Its accessible name starts with it. */\n label: string;\n /** The headline figure, such as \"1,284\". Scrubbing replaces it with the scrubbed point. */\n value?: string;\n /** Context beside the headline, such as \"+12% this week\". Coloured by `tone`. */\n change?: string;\n /** @default \"accent\" */\n tone?: SparklineTone;\n /** Aspect of the drawing; the line fills the width and keeps this ratio. @default 160 */\n width?: number;\n /** @default 52 */\n height?: number;\n /** One label per point, such as a date. It replaces the change while that point is scrubbed. */\n labels?: string[];\n /** Formats a scrubbed point for the headline. Defaults to a grouped number. */\n formatValue?: (value: number, index: number) => string;\n /** A faint fill under the line. @default true */\n area?: boolean;\n /** Pointer and keyboard scrubbing through the points. @default true */\n interactive?: boolean;\n /** Extra classes for the root. Merged last. */\n className?: string;\n}\n\ntype Point = [number, number];\n\n/** Space kept above the highest and below the lowest point, and half the line width. */\nconst INSET = 5;\nconst CAP = 1.25;\nconst GRID = Array.from({ length: 161 }, (_, index) => index / 160);\nconst GROUPED = new Intl.NumberFormat(\"en-US\");\n/** The first draw: one stroke, slow at both ends like a pen (480ms x 1.75). */\nexport const SPARKLINE_DRAW_MS = 840;\n\n/** A smooth line through every point, sampled densely in unit space (x and y 0 to 1, y up). Monotone cubic tangents keep it from swinging past the data. */\nfunction curveFor(data: number[]): Point[] {\n const min = Math.min(...data);\n const span = Math.max(...data) - min;\n const ys = data.map((value) => (span ? (value - min) / span : 0.5));\n const last = ys.length - 1;\n const step = 1 / last;\n const slopes = ys.slice(1).map((y, index) => (y - ys[index]!) / step);\n const tangents = ys.map((_, index) =>\n index === 0 || index === last\n ? slopes[0]!\n : (Math.sign(slopes[index - 1]!) + Math.sign(slopes[index]!)) *\n Math.min(\n Math.abs(slopes[index - 1]!),\n Math.abs(slopes[index]!),\n Math.abs(slopes[index - 1]! + slopes[index]!) / 4,\n ) || 0,\n );\n if (last > 1) {\n tangents[0] = (3 * slopes[0]! - tangents[1]!) / 2;\n tangents[last] = (3 * slopes[last - 1]! - tangents[last - 1]!) / 2;\n }\n const samples = Math.max(1, Math.min(16, Math.round(128 / last)));\n const points: Point[] = [];\n for (let index = 0; index < last; index++) {\n for (let sample = 0; sample < samples; sample++) {\n const t = sample / samples;\n const t2 = t * t;\n const t3 = t2 * t;\n const y =\n (2 * t3 - 3 * t2 + 1) * ys[index]! +\n (t3 - 2 * t2 + t) * step * tangents[index]! +\n (3 * t2 - 2 * t3) * ys[index + 1]! +\n (t3 - t2) * step * tangents[index + 1]!;\n points.push([(index + t) * step, Math.min(1, Math.max(0, y))]);\n }\n }\n points.push([1, ys[last]!]);\n return points;\n}\n\n/** Height of the sampled line at x, straight between samples. */\nfunction heightAt(shape: Point[], x: number) {\n let low = 0;\n let high = shape.length - 1;\n if (x <= shape[low]![0]) return shape[low]![1];\n if (x >= shape[high]![0]) return shape[high]![1];\n while (high - low > 1) {\n const middle = (low + high) >> 1;\n if (shape[middle]![0] < x) low = middle;\n else high = middle;\n }\n const [x0, y0] = shape[low]!;\n const [x1, y1] = shape[high]!;\n return x1 === x0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / (x1 - x0);\n}\n\n/** Every drawn string for one frame, in the chart's own pixels so strokes and dots stay round at any width. */\nfunction geometryOf(shape: Point[], w: number, h: number, at: number) {\n const toY = (y: number) => INSET + (1 - y) * (h - INSET * 2);\n const pairs = shape.map(([x, y]) => `${(x * w).toFixed(2)},${toY(y).toFixed(2)}`);\n const cx = at * w;\n const cy = toY(heightAt(shape, at));\n return {\n line: pairs.join(\" \"),\n trail: `M${pairs.join(\"L\")}`,\n fill: `M${pairs.join(\"L\")}L${w},${h}L0,${h}Z`,\n dot: `${cx},${cy} ${cx},${cy}`,\n cx,\n };\n}\n\nconst TONE: Record<SparklineTone, string> = {\n accent: \"text-primary-strong [--spark-stroke:var(--color-primary)]\",\n success: \"text-success-strong [--spark-stroke:var(--color-success-strong)]\",\n warning: \"text-warning-strong [--spark-stroke:var(--color-warning-strong)]\",\n danger: \"text-destructive-strong [--spark-stroke:var(--color-destructive-strong)]\",\n};\n\nexport const Sparkline = forwardRef<HTMLElement, SparklineProps>(function Sparkline(\n {\n data,\n label,\n value,\n change,\n tone = \"accent\",\n width = 160,\n height = 52,\n labels,\n formatValue,\n area = true,\n interactive = true,\n className,\n },\n ref,\n) {\n const series = data.length > 1 ? data : [data[0] ?? 0, data[0] ?? 0];\n const last = series.length - 1;\n const key = series.join(\",\");\n const target = useMemo(() => curveFor(key.split(\",\").map(Number)), [key]);\n const uid = `spark${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n const figure = useRef<HTMLElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, figure), [ref]);\n const plot = useRef<HTMLDivElement>(null);\n const svg = useRef<SVGSVGElement>(null);\n const shape = useRef(target);\n const cursorAt = useRef(1);\n const box = useRef({ w: width, h: height });\n const [size, setSize] = useState({ w: width, h: height });\n const [drawn, setDrawn] = useState(false);\n const [landed, setLanded] = useState(false);\n const [active, setActive] = useState<number | null>(null);\n const index = active === null ? null : Math.min(active, last);\n const [moved, setMoved] = useState({ index, dir: 1 });\n if (moved.index !== index) {\n setMoved({\n index,\n dir: index === null || moved.index === null ? 1 : Math.sign(index - moved.index) || 1,\n });\n }\n\n // The first frame, also what the server renders.\n const [first] = useState(() => geometryOf(target, width, height, 1));\n\n /** One writer for every drawn attribute, straight onto the elements: no re-render per frame. */\n const paint = useCallback(() => {\n const root = svg.current;\n if (!root) return;\n const { w, h } = box.current;\n const g = geometryOf(shape.current, w, h, cursorAt.current);\n const q = (name: string) => root.querySelector<SVGElement>(`[data-part=\"${name}\"]`);\n q(\"line\")?.setAttribute(\"points\", g.line);\n q(\"trail\")?.setAttribute(\"d\", g.trail);\n q(\"area\")?.setAttribute(\"d\", g.fill);\n q(\"dot\")?.setAttribute(\"points\", g.dot);\n q(\"lit\")?.setAttribute(\"width\", String(g.cx + CAP + 16));\n q(\"dim\")?.setAttribute(\"x\", String(g.cx + CAP));\n q(\"cursor\")?.setAttribute(\"x1\", String(g.cx));\n q(\"cursor\")?.setAttribute(\"x2\", String(g.cx));\n }, []);\n\n // The chart draws in its own pixels: measure the width, keep the height in proportion.\n useIsomorphicLayoutEffect(() => {\n const node = plot.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const w = node.clientWidth;\n if (!w || Math.abs(box.current.w - w) < 0.5) return;\n box.current = { w, h: (w * height) / width };\n setSize(box.current);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, [height, width]);\n useIsomorphicLayoutEffect(paint, [size, paint]);\n\n // The line draws once it is half in view; the fill follows the pen, then the end dot lands.\n useEffect(() => {\n const node = figure.current;\n if (!node) return;\n if (prefersReducedMotion() || typeof IntersectionObserver === \"undefined\") {\n setDrawn(true);\n setLanded(true);\n return;\n }\n let timer = 0;\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry?.isIntersecting) return;\n observer.disconnect();\n setDrawn(true);\n timer = window.setTimeout(() => setLanded(true), SPARKLINE_DRAW_MS);\n },\n { threshold: 0.5 },\n );\n observer.observe(node);\n return () => {\n observer.disconnect();\n window.clearTimeout(timer);\n };\n }, []);\n\n // New data morphs the line from the shape on screen, sampled on shared x positions so a series that gains or loses points still flows.\n useEffect(() => {\n const from = shape.current;\n if (from === target) return;\n const xs = [...new Set([...GRID, ...target.map(([x]) => x)])].sort((a, b) => a - b);\n const start = xs.map((x) => heightAt(from, x));\n const goal = xs.map((x) => heightAt(target, x));\n return tween(\n SMOOTH,\n (p) => {\n shape.current = xs.map((x, at): Point => [x, start[at]! + (goal[at]! - start[at]!) * p]);\n paint();\n },\n () => {\n shape.current = target;\n paint();\n },\n );\n }, [paint, target]);\n\n // The cursor springs to the scrubbed point and rides the line; leaving sends it home to the latest value.\n useEffect(() => {\n const from = cursorAt.current;\n const to = index === null ? 1 : index / last;\n if (from === to) return;\n return tween(SNAPPY, (p) => {\n cursorAt.current = from + (to - from) * p;\n paint();\n });\n }, [index, last, paint]);\n\n const scrubbing = index !== null;\n const format = (at: number) =>\n formatValue ? formatValue(series[at]!, at) : GROUPED.format(series[at]!);\n const point = format(index ?? last);\n const when = labels?.[index ?? last];\n const headline = value === undefined ? undefined : scrubbing ? point : value;\n const aside =\n index === null\n ? (change ?? \"\")\n : value === undefined\n ? [point, when].filter(Boolean).join(\" · \")\n : (when ?? `${index + 1} of ${last + 1}`);\n const description = `${label}: ${value ?? series[last]}. ${change ?? \"\"}`.trim();\n\n const pointAt = (clientX: number) => {\n const rect = plot.current?.getBoundingClientRect();\n return rect?.width\n ? Math.max(0, Math.min(last, Math.round(((clientX - rect.left) / rect.width) * last)))\n : last;\n };\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"Escape\" && scrubbing) {\n event.preventDefault();\n setActive(null);\n return;\n }\n const from = index ?? last;\n const page = Math.max(1, Math.round((last + 1) / 6));\n const next = (\n {\n ArrowLeft: from - 1,\n ArrowDown: from - 1,\n ArrowRight: from + 1,\n ArrowUp: from + 1,\n PageDown: from - page,\n PageUp: from + page,\n Home: 0,\n End: last,\n } as Record<string, number>\n )[event.key];\n if (next === undefined) return;\n event.preventDefault();\n setActive(Math.max(0, Math.min(last, next)));\n };\n const scrub = interactive\n ? {\n role: \"slider\",\n tabIndex: 0,\n \"aria-label\": `${label}, explore values`,\n \"aria-valuemin\": 1,\n \"aria-valuemax\": last + 1,\n \"aria-valuenow\": (index ?? last) + 1,\n \"aria-valuetext\": `${when ?? (index === null ? \"Latest\" : `Point ${index + 1} of ${last + 1}`)}: ${point}`,\n onPointerDown: (event: PointerEvent<HTMLDivElement>) => setActive(pointAt(event.clientX)),\n onPointerMove: (event: PointerEvent<HTMLDivElement>) => {\n if (event.pointerType === \"mouse\" || event.buttons) setActive(pointAt(event.clientX));\n },\n onPointerUp: (event: PointerEvent<HTMLDivElement>) => {\n if (event.pointerType !== \"mouse\") setActive(null);\n },\n onPointerLeave: () => setActive(null),\n onPointerCancel: () => setActive(null),\n onBlur: () => setActive(null),\n onKeyDown,\n }\n : {};\n\n return (\n <figure\n ref={refs}\n data-slot=\"sparkline\"\n data-drawn={drawn || undefined}\n data-landed={landed || undefined}\n data-scrubbing={scrubbing || undefined}\n aria-label={description}\n className={cn(sparklineVariants(), className)}\n >\n <figcaption className=\"flex min-w-0 items-baseline gap-2 text-sm text-text-secondary\">\n <span data-slot=\"sparkline-label\" className=\"min-w-0 truncate\">\n {label}\n </span>\n {headline === undefined ? null : (\n <strong\n data-slot=\"sparkline-value\"\n className=\"ms-auto text-lg font-medium tabular-nums text-foreground\"\n >\n <SwapText swapKey={headline} dir={moved.dir} motion=\"rise\">\n {headline}\n </SwapText>\n </strong>\n )}\n <small\n data-slot=\"sparkline-change\"\n // The scrubbed date is context, not a change, so it turns neutral.\n className={cnAll(\n \"flex-none text-xs font-medium tabular-nums sparkline-change\",\n scrubbing ? \"text-text-secondary\" : TONE[tone],\n )}\n >\n <SwapText swapKey={aside} dir={moved.dir} motion=\"rise\">\n {aside}\n </SwapText>\n </small>\n </figcaption>\n <div\n ref={plot}\n data-slot=\"sparkline-plot\"\n data-interactive={interactive || undefined}\n className=\"sparkline-plot\"\n {...scrub}\n >\n <svg\n ref={svg}\n viewBox={`0 0 ${size.w} ${size.h}`}\n aria-hidden=\"true\"\n focusable=\"false\"\n className={cnAll(\"block h-auto w-full overflow-visible\", TONE[tone])}\n >\n <defs>\n <clipPath id={`${uid}-lit`} clipPathUnits=\"userSpaceOnUse\">\n <rect data-part=\"lit\" x={-16} y={-16} width={first.cx + CAP + 16} height={size.h + 32} />\n </clipPath>\n <clipPath id={`${uid}-dim`} clipPathUnits=\"userSpaceOnUse\">\n <rect data-part=\"dim\" x={first.cx + CAP} y={-16} width={size.w + 32} height={size.h + 32} />\n </clipPath>\n <clipPath id={`${uid}-fill`} clipPathUnits=\"userSpaceOnUse\">\n <rect\n data-part=\"reveal\"\n className=\"sparkline-reveal\"\n x={-16}\n y={-16}\n height={size.h + 32}\n style={{ width: drawn ? size.w + 32 : 0 }}\n />\n </clipPath>\n </defs>\n {area ? (\n <path data-part=\"area\" className=\"sparkline-area\" d={first.fill} clipPath={`url(#${uid}-fill)`} />\n ) : null}\n <line\n className=\"sparkline-baseline\"\n x1=\"0\"\n y1={size.h - 0.5}\n x2={size.w}\n y2={size.h - 0.5}\n />\n {/* While scrubbing, the stretch after the cursor dims, as if the later points have not happened yet. */}\n <path data-part=\"trail\" className=\"sparkline-trail\" d={first.trail} clipPath={`url(#${uid}-dim)`} />\n <polyline\n data-part=\"line\"\n className=\"sparkline-line\"\n points={first.line}\n pathLength={1}\n clipPath={`url(#${uid}-lit)`}\n />\n <line data-part=\"cursor\" className=\"sparkline-cursor\" x1={first.cx} x2={first.cx} y1={0} y2={size.h} />\n {/* A zero-length round-capped stroke is a true circle in the line's colour; it rests on the latest point and becomes the scrub handle. */}\n <polyline data-part=\"dot\" className=\"sparkline-dot\" points={first.dot} />\n </svg>\n </div>\n </figure>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sparkline.js","names":[],"sources":["../../src/sparkline/sparkline.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type PointerEvent,\n} from \"react\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { prefersReducedMotion, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { SMOOTH, SNAPPY, tween } from \"../lib/spring-tween.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Sparkline: a small trend line with its label, an optional headline value and\n * a line of context (the change). The line draws itself left to right the first\n * time it is seen, the fill follows the pen, and the end dot lands. New data\n * morphs the line from the shape on screen. Hovering, dragging or the arrow\n * keys scrub through the points: a cursor rides the line, the stretch after it\n * dims, and the headline and context show that point.\n *\n * A missing point (`null`, `undefined` or not a finite number) is a gap: the\n * line breaks around it and the scale ignores it. A point with missing\n * neighbours on both sides is drawn as a dot.\n *\n * Plain SVG, plain CSS transitions and one small spring tween for the two\n * things CSS cannot animate (the shape, and the cursor riding it). No chart or\n * animation library.\n */\nexport const sparklineVariants = cva(\"grid min-w-0 gap-3 m-0 box-border font-sans text-foreground\");\n\nexport type SparklineTone = \"accent\" | \"success\" | \"warning\" | \"danger\";\nexport type SparklineSize = \"sm\" | \"md\";\n\nexport interface SparklineProps {\n /**\n * The series, oldest first. One value draws a flat line. A missing point\n * (`null`, `undefined` or not a finite number) breaks the line, is left out\n * of the scale and reads as `missingLabel` when scrubbed.\n */\n data: (number | null | undefined)[];\n /** What the line is. Its accessible name starts with it. */\n label: string;\n /** The headline figure, such as \"1,284\". Scrubbing replaces it with the scrubbed point. */\n value?: string;\n /** Context beside the headline, such as \"+12% this week\". Coloured by `tone`. */\n change?: string;\n /** @default \"accent\" */\n tone?: SparklineTone;\n /**\n * Any CSS colour for the line, its fill and its dot, such as a series token\n * (`var(--color-chart-2)`). Overrides the tone's line colour; the change text\n * keeps the tone's legible colour.\n */\n color?: string;\n /**\n * Hides the caption (label, headline and change), leaving the line alone,\n * for a table cell or a card that shows its own figures. The label still\n * names the figure and the slider. @default false\n */\n captionHidden?: boolean;\n /** `\"sm\"` draws a thinner line and smaller dot, for a short line in a table cell. @default \"md\" */\n size?: SparklineSize;\n /** Read in place of a missing point's value. @default \"No data\" */\n missingLabel?: string;\n /** Aspect of the drawing; the line fills the width and keeps this ratio. @default 160 */\n width?: number;\n /** @default 52 */\n height?: number;\n /** One label per point, such as a date. It replaces the change while that point is scrubbed. */\n labels?: string[];\n /** Formats a scrubbed point for the headline. Defaults to a grouped number. */\n formatValue?: (value: number, index: number) => string;\n /** A faint fill under the line. @default true */\n area?: boolean;\n /** Pointer and keyboard scrubbing through the points. @default true */\n interactive?: boolean;\n /** Extra classes for the root. Merged last. */\n className?: string;\n}\n\ntype Point = [number, number];\n\n/** Space kept above the highest and below the lowest point, and half the line width. */\nconst INSET: Record<SparklineSize, number> = { md: 5, sm: 3 };\nconst CAP = 1.25;\n/** Radius of a lone point's dot, about the line's width. */\nconst DOT = 2.5;\nconst GRID = Array.from({ length: 161 }, (_, index) => index / 160);\nconst GROUPED = new Intl.NumberFormat(\"en-US\");\n/** The first draw: one stroke, slow at both ends like a pen (480ms x 1.75). */\nexport const SPARKLINE_DRAW_MS = 840;\n\n/** A smooth line through every point, sampled densely in unit space (x and y 0 to 1, y up). Monotone cubic tangents keep it from swinging past the data. */\nfunction curveFor(data: number[]): Point[] {\n const min = Math.min(...data);\n const span = Math.max(...data) - min;\n const ys = data.map((value) => (span ? (value - min) / span : 0.5));\n const last = ys.length - 1;\n const step = 1 / last;\n const slopes = ys.slice(1).map((y, index) => (y - ys[index]!) / step);\n const tangents = ys.map((_, index) =>\n index === 0 || index === last\n ? slopes[0]!\n : (Math.sign(slopes[index - 1]!) + Math.sign(slopes[index]!)) *\n Math.min(\n Math.abs(slopes[index - 1]!),\n Math.abs(slopes[index]!),\n Math.abs(slopes[index - 1]! + slopes[index]!) / 4,\n ) || 0,\n );\n if (last > 1) {\n tangents[0] = (3 * slopes[0]! - tangents[1]!) / 2;\n tangents[last] = (3 * slopes[last - 1]! - tangents[last - 1]!) / 2;\n }\n const samples = Math.max(1, Math.min(16, Math.round(128 / last)));\n const points: Point[] = [];\n for (let index = 0; index < last; index++) {\n for (let sample = 0; sample < samples; sample++) {\n const t = sample / samples;\n const t2 = t * t;\n const t3 = t2 * t;\n const y =\n (2 * t3 - 3 * t2 + 1) * ys[index]! +\n (t3 - 2 * t2 + t) * step * tangents[index]! +\n (3 * t2 - 2 * t3) * ys[index + 1]! +\n (t3 - t2) * step * tangents[index + 1]!;\n points.push([(index + t) * step, Math.min(1, Math.max(0, y))]);\n }\n }\n points.push([1, ys[last]!]);\n return points;\n}\n\n/** Fills each missing point straight between its present neighbours (or with the nearest one at the ends), so a gapped line still has a shape to draw and morph. */\nfunction fillGaps(values: (number | null)[]): number[] {\n const present = values.flatMap((value, index) => (value === null ? [] : [index]));\n if (!present.length) return values.map(() => 0);\n return values.map((value, index) => {\n if (value !== null) return value;\n const after = present.find((at) => at > index);\n const before = present.findLast((at) => at < index);\n if (before === undefined) return values[after!]!;\n if (after === undefined) return values[before]!;\n return (\n values[before]! + ((values[after]! - values[before]!) * (index - before)) / (after - before)\n );\n });\n}\n\n/** The stretches of x (0 to 1) where the line is drawn: from each present point to the next, split where missing points sit between. */\nfunction runsFor(values: (number | null)[]): [from: number, to: number][] {\n const last = values.length - 1;\n const runs: [number, number][] = [];\n values.forEach((value, index) => {\n if (value === null) return;\n const run = runs[runs.length - 1];\n if (run && values[index - 1] !== null && index > 0) run[1] = index / last;\n else runs.push([index / last, index / last]);\n });\n return runs;\n}\n\n/** Height of the sampled line at x, straight between samples. */\nfunction heightAt(shape: Point[], x: number) {\n let low = 0;\n let high = shape.length - 1;\n if (x <= shape[low]![0]) return shape[low]![1];\n if (x >= shape[high]![0]) return shape[high]![1];\n while (high - low > 1) {\n const middle = (low + high) >> 1;\n if (shape[middle]![0] < x) low = middle;\n else high = middle;\n }\n const [x0, y0] = shape[low]!;\n const [x1, y1] = shape[high]!;\n return x1 === x0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / (x1 - x0);\n}\n\n/** Every drawn string for one frame, in the chart's own pixels so strokes and dots stay round at any width. */\nfunction geometryOf(shape: Point[], w: number, h: number, at: number, inset: number) {\n const toY = (y: number) => inset + (1 - y) * (h - inset * 2);\n const pairs = shape.map(([x, y]) => `${(x * w).toFixed(2)},${toY(y).toFixed(2)}`);\n const cx = at * w;\n const cy = toY(heightAt(shape, at));\n return {\n line: pairs.join(\" \"),\n trail: `M${pairs.join(\"L\")}`,\n fill: `M${pairs.join(\"L\")}L${w},${h}L0,${h}Z`,\n dot: `${cx},${cy} ${cx},${cy}`,\n cx,\n };\n}\n\nconst TONE: Record<SparklineTone, string> = {\n accent: \"text-primary-strong [--spark-stroke:var(--color-primary)]\",\n success: \"text-success-strong [--spark-stroke:var(--color-success-strong)]\",\n warning: \"text-warning-strong [--spark-stroke:var(--color-warning-strong)]\",\n danger: \"text-destructive-strong [--spark-stroke:var(--color-destructive-strong)]\",\n};\n\nexport const Sparkline = forwardRef<HTMLElement, SparklineProps>(function Sparkline(\n {\n data,\n label,\n value,\n change,\n tone = \"accent\",\n color,\n captionHidden = false,\n size: scale = \"md\",\n missingLabel = \"No data\",\n width = 160,\n height = 52,\n labels,\n formatValue,\n area = true,\n interactive = true,\n className,\n },\n ref,\n) {\n const points = data.map((point) =>\n typeof point === \"number\" && Number.isFinite(point) ? point : null,\n );\n const values = points.length > 1 ? points : [points[0] ?? null, points[0] ?? null];\n const series = fillGaps(values);\n const last = series.length - 1;\n const key = series.join(\",\");\n const target = useMemo(() => curveFor(key.split(\",\").map(Number)), [key]);\n const gapped = values.includes(null);\n const inset = INSET[scale];\n const uid = `spark${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n const figure = useRef<HTMLElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, figure), [ref]);\n const plot = useRef<HTMLDivElement>(null);\n const svg = useRef<SVGSVGElement>(null);\n const shape = useRef(target);\n const cursorAt = useRef(1);\n const box = useRef({ w: width, h: height, inset });\n const [size, setSize] = useState({ w: width, h: height });\n const [drawn, setDrawn] = useState(false);\n const [landed, setLanded] = useState(false);\n const [active, setActive] = useState<number | null>(null);\n const index = active === null ? null : Math.min(active, last);\n const [moved, setMoved] = useState({ index, dir: 1 });\n if (moved.index !== index) {\n setMoved({\n index,\n dir: index === null || moved.index === null ? 1 : Math.sign(index - moved.index) || 1,\n });\n }\n\n // The first frame, also what the server renders.\n const [first] = useState(() => geometryOf(target, width, height, 1, inset));\n\n /** One writer for every drawn attribute, straight onto the elements: no re-render per frame. */\n const paint = useCallback(() => {\n const root = svg.current;\n if (!root) return;\n const { w, h, inset: room } = box.current;\n const g = geometryOf(shape.current, w, h, cursorAt.current, room);\n const q = (name: string) => root.querySelector<SVGElement>(`[data-part=\"${name}\"]`);\n q(\"line\")?.setAttribute(\"points\", g.line);\n q(\"trail\")?.setAttribute(\"d\", g.trail);\n q(\"area\")?.setAttribute(\"d\", g.fill);\n q(\"dot\")?.setAttribute(\"points\", g.dot);\n // A lone point's dot rides the morphing line, at the height the line has under it.\n for (const node of root.querySelectorAll<SVGElement>('[data-part=\"point\"]')) {\n const y = heightAt(shape.current, Number(node.dataset.at));\n node.setAttribute(\"cy\", String(room + (1 - y) * (h - room * 2)));\n }\n q(\"lit\")?.setAttribute(\"width\", String(g.cx + CAP + 16));\n q(\"dim\")?.setAttribute(\"x\", String(g.cx + CAP));\n q(\"cursor\")?.setAttribute(\"x1\", String(g.cx));\n q(\"cursor\")?.setAttribute(\"x2\", String(g.cx));\n }, []);\n\n // The chart draws in its own pixels: measure the width, keep the height in proportion.\n useIsomorphicLayoutEffect(() => {\n const node = plot.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const w = node.clientWidth;\n if (!w || Math.abs(box.current.w - w) < 0.5) return;\n box.current = { ...box.current, w, h: (w * height) / width };\n setSize(box.current);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, [height, width]);\n useIsomorphicLayoutEffect(() => {\n box.current.inset = inset;\n paint();\n }, [size, inset, paint]);\n\n // The line draws once it is half in view; the fill follows the pen, then the end dot lands.\n useEffect(() => {\n const node = figure.current;\n if (!node) return;\n if (prefersReducedMotion() || typeof IntersectionObserver === \"undefined\") {\n setDrawn(true);\n setLanded(true);\n return;\n }\n let timer = 0;\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry?.isIntersecting) return;\n observer.disconnect();\n setDrawn(true);\n timer = window.setTimeout(() => setLanded(true), SPARKLINE_DRAW_MS);\n },\n { threshold: 0.5 },\n );\n observer.observe(node);\n return () => {\n observer.disconnect();\n window.clearTimeout(timer);\n };\n }, []);\n\n // New data morphs the line from the shape on screen, sampled on shared x positions so a series that gains or loses points still flows.\n useEffect(() => {\n const from = shape.current;\n if (from === target) return;\n const xs = [...new Set([...GRID, ...target.map(([x]) => x)])].sort((a, b) => a - b);\n const start = xs.map((x) => heightAt(from, x));\n const goal = xs.map((x) => heightAt(target, x));\n return tween(\n SMOOTH,\n (p) => {\n shape.current = xs.map((x, at): Point => [x, start[at]! + (goal[at]! - start[at]!) * p]);\n paint();\n },\n () => {\n shape.current = target;\n paint();\n },\n );\n }, [paint, target]);\n\n // The cursor springs to the scrubbed point and rides the line; leaving sends it home to the latest value.\n useEffect(() => {\n const from = cursorAt.current;\n const to = index === null ? 1 : index / last;\n if (from === to) return;\n return tween(SNAPPY, (p) => {\n cursorAt.current = from + (to - from) * p;\n paint();\n });\n }, [index, last, paint]);\n\n const scrubbing = index !== null;\n const format = (at: number) =>\n values[at] === null\n ? missingLabel\n : formatValue\n ? formatValue(values[at]!, at)\n : GROUPED.format(values[at]!);\n const point = format(index ?? last);\n const when = labels?.[index ?? last];\n const headline = value === undefined ? undefined : scrubbing ? point : value;\n const aside =\n index === null\n ? (change ?? \"\")\n : value === undefined\n ? [point, when].filter(Boolean).join(\" · \")\n : (when ?? `${index + 1} of ${last + 1}`);\n const description = `${label}: ${value ?? values[last] ?? missingLabel}. ${change ?? \"\"}`.trim();\n\n // Where the line is drawn, in the chart's pixels. Each stretch reaches half a line width past its end points so their round caps show; a lone point keeps room for its dot.\n const runs = gapped ? runsFor(values) : [];\n const gaps = gapped ? `url(#${uid}-gaps)` : undefined;\n const toY = (y: number) => inset + (1 - y) * (size.h - inset * 2);\n // At the height of the line on screen, not of its target: `paint` carries the dot from there with the morph.\n const lone = runs\n .filter(([from, to]) => from === to)\n .map(([at]) => ({ at, cx: at * size.w, cy: toY(heightAt(shape.current, at)) }));\n\n const pointAt = (clientX: number) => {\n const rect = plot.current?.getBoundingClientRect();\n return rect?.width\n ? Math.max(0, Math.min(last, Math.round(((clientX - rect.left) / rect.width) * last)))\n : last;\n };\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"Escape\" && scrubbing) {\n event.preventDefault();\n setActive(null);\n return;\n }\n const from = index ?? last;\n const page = Math.max(1, Math.round((last + 1) / 6));\n const next = (\n {\n ArrowLeft: from - 1,\n ArrowDown: from - 1,\n ArrowRight: from + 1,\n ArrowUp: from + 1,\n PageDown: from - page,\n PageUp: from + page,\n Home: 0,\n End: last,\n } as Record<string, number>\n )[event.key];\n if (next === undefined) return;\n event.preventDefault();\n setActive(Math.max(0, Math.min(last, next)));\n };\n const scrub = interactive\n ? {\n role: \"slider\",\n tabIndex: 0,\n \"aria-label\": `${label}, explore values`,\n \"aria-valuemin\": 1,\n \"aria-valuemax\": last + 1,\n \"aria-valuenow\": (index ?? last) + 1,\n \"aria-valuetext\": `${when ?? (index === null ? \"Latest\" : `Point ${index + 1} of ${last + 1}`)}: ${point}`,\n onPointerDown: (event: PointerEvent<HTMLDivElement>) => setActive(pointAt(event.clientX)),\n onPointerMove: (event: PointerEvent<HTMLDivElement>) => {\n if (event.pointerType === \"mouse\" || event.buttons) setActive(pointAt(event.clientX));\n },\n onPointerUp: (event: PointerEvent<HTMLDivElement>) => {\n if (event.pointerType !== \"mouse\") setActive(null);\n },\n onPointerLeave: () => setActive(null),\n onPointerCancel: () => setActive(null),\n onBlur: () => setActive(null),\n onKeyDown,\n }\n : {};\n\n return (\n <figure\n ref={refs}\n data-slot=\"sparkline\"\n data-drawn={drawn || undefined}\n data-landed={landed || undefined}\n data-scrubbing={scrubbing || undefined}\n data-size={scale === \"md\" ? undefined : scale}\n aria-label={description}\n className={cn(sparklineVariants(), className)}\n >\n {captionHidden ? null : (\n <figcaption className=\"flex min-w-0 items-baseline gap-2 text-sm text-text-secondary\">\n <span data-slot=\"sparkline-label\" className=\"min-w-0 truncate\">\n {label}\n </span>\n {headline === undefined ? null : (\n <strong\n data-slot=\"sparkline-value\"\n className=\"ms-auto text-lg font-medium tabular-nums text-foreground\"\n >\n <SwapText swapKey={headline} dir={moved.dir} motion=\"rise\">\n {headline}\n </SwapText>\n </strong>\n )}\n <small\n data-slot=\"sparkline-change\"\n // The scrubbed date is context, not a change, so it turns neutral.\n className={cnAll(\n \"flex-none text-xs font-medium tabular-nums sparkline-change\",\n scrubbing ? \"text-text-secondary\" : TONE[tone],\n )}\n >\n <SwapText swapKey={aside} dir={moved.dir} motion=\"rise\">\n {aside}\n </SwapText>\n </small>\n </figcaption>\n )}\n <div\n ref={plot}\n data-slot=\"sparkline-plot\"\n data-interactive={interactive || undefined}\n className=\"sparkline-plot\"\n {...scrub}\n >\n <svg\n ref={svg}\n viewBox={`0 0 ${size.w} ${size.h}`}\n aria-hidden=\"true\"\n focusable=\"false\"\n className={cnAll(\"block h-auto w-full overflow-visible\", TONE[tone])}\n style={color ? ({ color, \"--spark-stroke\": color } as CSSProperties) : undefined}\n >\n <defs>\n <clipPath id={`${uid}-lit`} clipPathUnits=\"userSpaceOnUse\">\n <rect data-part=\"lit\" x={-16} y={-16} width={first.cx + CAP + 16} height={size.h + 32} />\n </clipPath>\n <clipPath id={`${uid}-dim`} clipPathUnits=\"userSpaceOnUse\">\n <rect data-part=\"dim\" x={first.cx + CAP} y={-16} width={size.w + 32} height={size.h + 32} />\n </clipPath>\n <clipPath id={`${uid}-fill`} clipPathUnits=\"userSpaceOnUse\">\n <rect\n data-part=\"reveal\"\n className=\"sparkline-reveal\"\n x={-16}\n y={-16}\n height={size.h + 32}\n style={{ width: drawn ? size.w + 32 : 0 }}\n />\n </clipPath>\n {gapped ? (\n <mask\n id={`${uid}-gaps`}\n maskUnits=\"userSpaceOnUse\"\n x={-16}\n y={-16}\n width={size.w + 32}\n height={size.h + 32}\n >\n {runs.map(([from, to]) => {\n const reach = from === to ? DOT : CAP;\n const x = from === 0 ? -16 : from * size.w - reach;\n const end = to === 1 ? size.w + 16 : to * size.w + reach;\n return (\n <rect\n key={from}\n data-part=\"run\"\n x={x}\n y={-16}\n width={end - x}\n height={size.h + 32}\n fill=\"white\"\n />\n );\n })}\n </mask>\n ) : null}\n </defs>\n {area ? (\n <path\n data-part=\"area\"\n className=\"sparkline-area\"\n d={first.fill}\n clipPath={`url(#${uid}-fill)`}\n mask={gaps}\n />\n ) : null}\n <line\n className=\"sparkline-baseline\"\n x1=\"0\"\n y1={size.h - 0.5}\n x2={size.w}\n y2={size.h - 0.5}\n />\n {/* While scrubbing, the stretch after the cursor dims, as if the later points have not happened yet. */}\n <path\n data-part=\"trail\"\n className=\"sparkline-trail\"\n d={first.trail}\n clipPath={`url(#${uid}-dim)`}\n mask={gaps}\n />\n <polyline\n data-part=\"line\"\n className=\"sparkline-line\"\n points={first.line}\n pathLength={1}\n clipPath={`url(#${uid}-lit)`}\n mask={gaps}\n />\n {lone.map(({ at, cx, cy }) => (\n <circle\n key={cx}\n data-part=\"point\"\n data-at={at}\n className=\"sparkline-point\"\n cx={cx}\n cy={cy}\n r={DOT}\n />\n ))}\n <line data-part=\"cursor\" className=\"sparkline-cursor\" x1={first.cx} x2={first.cx} y1={0} y2={size.h} />\n {/* A zero-length round-capped stroke is a true circle in the line's colour; it rests on the latest point and becomes the scrub handle. Inside a gap it is masked away. */}\n <polyline data-part=\"dot\" className=\"sparkline-dot\" points={first.dot} mask={gaps} />\n </svg>\n </div>\n </figure>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { StatCard, StatCardChangeTone, StatCardProps, StatCardTone, StatCardTrend, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
|
|
2
|
-
export { StatCard, type StatCardChangeTone, type StatCardProps, type StatCardTone, type StatCardTrend, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
|
1
|
+
import { StatCard, StatCardChangeTone, StatCardProps, StatCardTone, StatCardTrend, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSkeletonVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
|
|
2
|
+
export { StatCard, type StatCardChangeTone, type StatCardProps, type StatCardTone, type StatCardTrend, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSkeletonVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
package/dist/stat-card/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
|
|
2
|
-
export { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
|
1
|
+
import { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSkeletonVariants, statCardSubVariants, statCardValueVariants, statCardVariants } from "./stat-card.js";
|
|
2
|
+
export { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSkeletonVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
|
@@ -33,6 +33,12 @@ import { ReactNode } from "react";
|
|
|
33
33
|
* it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a
|
|
34
34
|
* React Aria ToggleButton with `aria-pressed` and one shared selected look).
|
|
35
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
|
+
*
|
|
36
42
|
* Every string in the cva blocks below is a STATIC literal. Tailwind scans
|
|
37
43
|
* source text, so a class assembled at runtime is invisible to it and would
|
|
38
44
|
* simply never be emitted.
|
|
@@ -45,11 +51,19 @@ export declare const statCardVariants: (props?: ({
|
|
|
45
51
|
export declare const statCardHeaderVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
46
52
|
export declare const statCardLabelVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
47
53
|
export declare const statCardValueVariants: (props?: ({
|
|
48
|
-
tone?: "
|
|
54
|
+
tone?: "neutral" | "warning" | "positive" | "negative" | "attention" | null | undefined;
|
|
49
55
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
50
56
|
/** An optional icon before the label. Decorative: the label names the figure. */
|
|
51
57
|
export declare const statCardIconVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
52
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" | "sub" | "value" | null | undefined;
|
|
66
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
53
67
|
export type StatCardTone = "neutral" | "positive" | "negative" | "warning" | "attention";
|
|
54
68
|
/** Which way is good for this figure: up (`normal`), down (`inverse`), or neither (`neutral`). */
|
|
55
69
|
export type StatCardTrend = "normal" | "inverse" | "neutral";
|
|
@@ -111,6 +125,23 @@ export interface StatCardProps {
|
|
|
111
125
|
onPress?: () => void;
|
|
112
126
|
/** Hover text for the whole card. */
|
|
113
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;
|
|
114
145
|
/** Anything under the context line. */
|
|
115
146
|
children?: ReactNode;
|
|
116
147
|
/** Layout classes for the card (margins, grid placement). */
|
|
@@ -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
|
|
@@ -41,6 +42,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
41
42
|
* it), a button (`onPress`), or a filter toggle (`isSelected` + `onPress`, a
|
|
42
43
|
* React Aria ToggleButton with `aria-pressed` and one shared selected look).
|
|
43
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
|
+
*
|
|
44
51
|
* Every string in the cva blocks below is a STATIC literal. Tailwind scans
|
|
45
52
|
* source text, so a class assembled at runtime is invisible to it and would
|
|
46
53
|
* simply never be emitted.
|
|
@@ -92,6 +99,18 @@ tone: {
|
|
|
92
99
|
/** An optional icon before the label. Decorative: the label names the figure. */
|
|
93
100
|
const statCardIconVariants = cva("flex shrink-0 items-center text-muted-foreground [&_svg]:size-4 [&_svg]:shrink-0");
|
|
94
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
|
+
} } });
|
|
95
114
|
/** The number in a piece of text, for the direction a swap moves: a larger one rises from below. */
|
|
96
115
|
const amountIn = (text) => Number(text.replace(/,/g, "").match(/-?\d+(?:\.\d+)?/)?.[0] ?? NaN);
|
|
97
116
|
/** 1 when the new text holds a number that did not shrink, -1 when it did. Updated during render. */
|
|
@@ -162,6 +181,8 @@ const BADGE_TONE = {
|
|
|
162
181
|
negative: "danger",
|
|
163
182
|
neutral: "neutral"
|
|
164
183
|
};
|
|
184
|
+
/** The swap key of an empty label's placeholder: a key no label text can be. */
|
|
185
|
+
const LOADING_KEY = "\0loading";
|
|
165
186
|
/** Only the `data-*` attributes, so nothing else in the rest object reaches the DOM. */
|
|
166
187
|
const dataAttributes = (rest) => Object.fromEntries(Object.entries(rest).filter(([key]) => key.startsWith("data-")));
|
|
167
188
|
/**
|
|
@@ -174,10 +195,17 @@ const dataAttributes = (rest) => Object.fromEntries(Object.entries(rest).filter(
|
|
|
174
195
|
* <StatCard label="Collected" value="$9,800" tone="positive" href="/payments" />
|
|
175
196
|
* </div>
|
|
176
197
|
*/
|
|
177
|
-
const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend = "normal", changeTone, icon, 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) {
|
|
178
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
|
+
});
|
|
179
205
|
const meaning = change ? changeMeaning(change, trend, changeTone) : "neutral";
|
|
180
206
|
const body = /* @__PURE__ */ jsxs("span", {
|
|
207
|
+
"data-slot": "stat-card-body",
|
|
208
|
+
"aria-busy": isLoading || void 0,
|
|
181
209
|
className: "flex min-w-0 flex-1 flex-col",
|
|
182
210
|
title: description,
|
|
183
211
|
children: [
|
|
@@ -194,16 +222,27 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend
|
|
|
194
222
|
"data-slot": "stat-card-label",
|
|
195
223
|
className: cn(statCardLabelVariants(), "block"),
|
|
196
224
|
children: /* @__PURE__ */ jsx(Swap, {
|
|
197
|
-
text: label,
|
|
225
|
+
text: isLoading && label === "" ? LOADING_KEY : label,
|
|
198
226
|
className: "*:max-w-full",
|
|
199
227
|
children: /* @__PURE__ */ jsx("span", {
|
|
200
228
|
className: "block truncate",
|
|
201
229
|
title: label,
|
|
202
|
-
children: label
|
|
230
|
+
children: isLoading && label === "" ? placeholder("label") : label
|
|
203
231
|
})
|
|
204
232
|
})
|
|
205
233
|
}),
|
|
206
|
-
change ? /* @__PURE__ */ jsx(
|
|
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
|
|
244
|
+
})
|
|
245
|
+
}) : change ? /* @__PURE__ */ jsx(Badge, {
|
|
207
246
|
shape: "pill",
|
|
208
247
|
tone: BADGE_TONE[meaning],
|
|
209
248
|
appearance: meaning === "neutral" ? "outline" : "soft",
|
|
@@ -216,7 +255,10 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend
|
|
|
216
255
|
"data-slot": "stat-card-value",
|
|
217
256
|
"data-tone": tone ?? "neutral",
|
|
218
257
|
className: cn(statCardValueVariants({ tone }), "block"),
|
|
219
|
-
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, {
|
|
220
262
|
value: figure.value,
|
|
221
263
|
decimals: figure.decimals,
|
|
222
264
|
prefix: figure.prefix,
|
|
@@ -227,7 +269,7 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend
|
|
|
227
269
|
sub === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
228
270
|
"data-slot": "stat-card-sub",
|
|
229
271
|
className: cn(statCardSubVariants(), "block"),
|
|
230
|
-
children: typeof sub === "string" ? /* @__PURE__ */ jsx(Swap, {
|
|
272
|
+
children: isLoading ? placeholder("sub") : typeof sub === "string" ? /* @__PURE__ */ jsx(Swap, {
|
|
231
273
|
text: sub,
|
|
232
274
|
children: sub
|
|
233
275
|
}) : sub
|
|
@@ -242,6 +284,7 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend
|
|
|
242
284
|
const common = {
|
|
243
285
|
...dataAttributes(rest),
|
|
244
286
|
"data-slot": "stat-card",
|
|
287
|
+
"data-loading": isLoading || void 0,
|
|
245
288
|
"aria-label": rest["aria-label"]
|
|
246
289
|
};
|
|
247
290
|
if (isSelected !== void 0) return /* @__PURE__ */ jsx(ToggleButton, {
|
|
@@ -274,6 +317,6 @@ const StatCard = forwardRef(function StatCard({ label, value, sub, change, trend
|
|
|
274
317
|
});
|
|
275
318
|
});
|
|
276
319
|
//#endregion
|
|
277
|
-
export { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
|
320
|
+
export { StatCard, statCardHeaderVariants, statCardIconVariants, statCardLabelVariants, statCardSkeletonVariants, statCardSubVariants, statCardValueVariants, statCardVariants };
|
|
278
321
|
|
|
279
322
|
//# sourceMappingURL=stat-card.js.map
|