@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.
Files changed (149) hide show
  1. package/README.md +315 -4
  2. package/dist/alert/alert.d.ts +2 -2
  3. package/dist/alert/alert.js +1 -1
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert-dialog/alert-dialog.d.ts +14 -0
  6. package/dist/alert-dialog/alert-dialog.js +52 -27
  7. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  8. package/dist/announcement-bar/announcement-bar.d.ts +15 -4
  9. package/dist/announcement-bar/announcement-bar.js +31 -6
  10. package/dist/announcement-bar/announcement-bar.js.map +1 -1
  11. package/dist/avatar/avatar.d.ts +7 -0
  12. package/dist/avatar/avatar.js +33 -12
  13. package/dist/avatar/avatar.js.map +1 -1
  14. package/dist/badge/badge.d.ts +16 -1
  15. package/dist/badge/badge.js +12 -5
  16. package/dist/badge/badge.js.map +1 -1
  17. package/dist/bar-chart/bar-chart-series.d.ts +75 -0
  18. package/dist/bar-chart/bar-chart-series.js +401 -0
  19. package/dist/bar-chart/bar-chart-series.js.map +1 -0
  20. package/dist/bar-chart/bar-chart.d.ts +25 -1
  21. package/dist/bar-chart/bar-chart.js +26 -1
  22. package/dist/bar-chart/bar-chart.js.map +1 -1
  23. package/dist/bar-chart/index.d.ts +2 -1
  24. package/dist/button/button.d.ts +24 -2
  25. package/dist/button/button.js +62 -13
  26. package/dist/button/button.js.map +1 -1
  27. package/dist/card/card.js +1 -1
  28. package/dist/card/card.js.map +1 -1
  29. package/dist/checkbox/checkbox.d.ts +25 -1
  30. package/dist/checkbox/checkbox.js +103 -28
  31. package/dist/checkbox/checkbox.js.map +1 -1
  32. package/dist/checkbox/index.d.ts +2 -2
  33. package/dist/checkbox/index.js +2 -2
  34. package/dist/chip-group/chip-group.d.ts +11 -0
  35. package/dist/chip-group/chip-group.js +50 -34
  36. package/dist/chip-group/chip-group.js.map +1 -1
  37. package/dist/chip-group/index.d.ts +2 -2
  38. package/dist/code-block/code-block.d.ts +45 -0
  39. package/dist/code-block/code-block.js +74 -0
  40. package/dist/code-block/code-block.js.map +1 -0
  41. package/dist/code-block/index.d.ts +2 -0
  42. package/dist/code-block/index.js +2 -0
  43. package/dist/command-palette/command-palette.d.ts +128 -0
  44. package/dist/command-palette/command-palette.js +371 -0
  45. package/dist/command-palette/command-palette.js.map +1 -0
  46. package/dist/command-palette/index.d.ts +2 -0
  47. package/dist/command-palette/index.js +2 -0
  48. package/dist/copy-button/copy-button.d.ts +19 -3
  49. package/dist/copy-button/copy-button.js +7 -4
  50. package/dist/copy-button/copy-button.js.map +1 -1
  51. package/dist/dialog/dialog.js +1 -1
  52. package/dist/dialog/dialog.js.map +1 -1
  53. package/dist/empty-state/empty-state.d.ts +17 -0
  54. package/dist/empty-state/empty-state.js +9 -7
  55. package/dist/empty-state/empty-state.js.map +1 -1
  56. package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
  57. package/dist/file-drop-zone/file-drop-zone.js +26 -8
  58. package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
  59. package/dist/gauge/gauge.d.ts +18 -1
  60. package/dist/gauge/gauge.js +35 -21
  61. package/dist/gauge/gauge.js.map +1 -1
  62. package/dist/gauge/index.d.ts +2 -2
  63. package/dist/index.d.ts +14 -9
  64. package/dist/index.js +14 -10
  65. package/dist/info-tooltip/index.d.ts +2 -0
  66. package/dist/info-tooltip/index.js +2 -0
  67. package/dist/info-tooltip/info-tooltip.d.ts +35 -0
  68. package/dist/info-tooltip/info-tooltip.js +49 -0
  69. package/dist/info-tooltip/info-tooltip.js.map +1 -0
  70. package/dist/input/input.d.ts +7 -0
  71. package/dist/input/input.js +4 -2
  72. package/dist/input/input.js.map +1 -1
  73. package/dist/lib/field-shell.js +2 -6
  74. package/dist/lib/field-shell.js.map +1 -1
  75. package/dist/lib/required-marker.js +20 -0
  76. package/dist/lib/required-marker.js.map +1 -0
  77. package/dist/lib/selection-control.js +32 -1
  78. package/dist/lib/selection-control.js.map +1 -1
  79. package/dist/line-chart/index.d.ts +2 -2
  80. package/dist/line-chart/line-chart.d.ts +106 -3
  81. package/dist/line-chart/line-chart.js +185 -46
  82. package/dist/line-chart/line-chart.js.map +1 -1
  83. package/dist/mention-input/mention-input.js +1 -1
  84. package/dist/money-input/index.d.ts +2 -2
  85. package/dist/money-input/money-input.d.ts +14 -0
  86. package/dist/money-input/money-input.js +17 -7
  87. package/dist/money-input/money-input.js.map +1 -1
  88. package/dist/morph-select/morph-select.d.ts +8 -1
  89. package/dist/morph-select/morph-select.js +8 -5
  90. package/dist/morph-select/morph-select.js.map +1 -1
  91. package/dist/multi-select/multi-select.d.ts +31 -0
  92. package/dist/multi-select/multi-select.js +123 -6
  93. package/dist/multi-select/multi-select.js.map +1 -1
  94. package/dist/number-field/number-field.js +1 -1
  95. package/dist/password-field/password-field.js +1 -1
  96. package/dist/password-strength/password-strength.js +1 -1
  97. package/dist/phone-input/phone-input.js +1 -1
  98. package/dist/popover/popover.js +1 -1
  99. package/dist/popover/popover.js.map +1 -1
  100. package/dist/progress/index.d.ts +2 -2
  101. package/dist/progress/index.js +2 -2
  102. package/dist/progress/progress.d.ts +53 -2
  103. package/dist/progress/progress.js +146 -36
  104. package/dist/progress/progress.js.map +1 -1
  105. package/dist/radio-group/radio-group.d.ts +12 -3
  106. package/dist/radio-group/radio-group.js +62 -52
  107. package/dist/radio-group/radio-group.js.map +1 -1
  108. package/dist/rich-text-editor/rich-text-editor.js +1 -1
  109. package/dist/scroll-area/scroll-area.d.ts +1 -1
  110. package/dist/scrub-input/scrub-input.d.ts +11 -1
  111. package/dist/scrub-input/scrub-input.js +22 -10
  112. package/dist/scrub-input/scrub-input.js.map +1 -1
  113. package/dist/search-field/search-field.js +1 -1
  114. package/dist/select/select.d.ts +10 -1
  115. package/dist/select/select.js +4 -3
  116. package/dist/select/select.js.map +1 -1
  117. package/dist/separator/separator.d.ts +1 -1
  118. package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
  119. package/dist/sparkline/sparkline.d.ts +27 -2
  120. package/dist/sparkline/sparkline.js +115 -18
  121. package/dist/sparkline/sparkline.js.map +1 -1
  122. package/dist/stat-card/index.d.ts +2 -2
  123. package/dist/stat-card/index.js +2 -2
  124. package/dist/stat-card/stat-card.d.ts +32 -1
  125. package/dist/stat-card/stat-card.js +52 -9
  126. package/dist/stat-card/stat-card.js.map +1 -1
  127. package/dist/styles.css +1 -1
  128. package/dist/table/index.d.ts +2 -2
  129. package/dist/table/index.js +2 -2
  130. package/dist/table/table.d.ts +211 -3
  131. package/dist/table/table.js +473 -93
  132. package/dist/table/table.js.map +1 -1
  133. package/dist/tabs/tabs.d.ts +4 -2
  134. package/dist/tabs/tabs.js +7 -5
  135. package/dist/tabs/tabs.js.map +1 -1
  136. package/dist/tag-input/tag-input.js +1 -1
  137. package/dist/text-shimmer/text-shimmer.d.ts +18 -2
  138. package/dist/text-shimmer/text-shimmer.js +5 -2
  139. package/dist/text-shimmer/text-shimmer.js.map +1 -1
  140. package/dist/textarea/textarea.d.ts +7 -0
  141. package/dist/textarea/textarea.js +4 -2
  142. package/dist/textarea/textarea.js.map +1 -1
  143. package/dist/toast/toast.d.ts +2 -2
  144. package/dist/toggle-button/index.d.ts +2 -0
  145. package/dist/toggle-button/index.js +2 -0
  146. package/dist/toggle-button/toggle-button.d.ts +93 -0
  147. package/dist/toggle-button/toggle-button.js +199 -0
  148. package/dist/toggle-button/toggle-button.js.map +1 -0
  149. 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 = 5;
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) => INSET + (1 - y) * (h - 10);
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 series = data.length > 1 ? data : [data[0] ?? 0, data[0] ?? 0];
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(paint, [size, paint]);
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(series[at], at) : GROUPED.format(series[at]);
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 ?? series[last]}. ${change ?? ""}`.trim();
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 };
@@ -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?: "warning" | "neutral" | "positive" | "negative" | "attention" | null | undefined;
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(Badge, {
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: figure ? /* @__PURE__ */ jsx(AnimatedCounter, {
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