@aparte/plugin-cards 0.0.0-stage → 0.29.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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -2
  3. package/dist/carousel-CPy-0JGN.js +63 -0
  4. package/dist/carousel-CPy-0JGN.js.map +1 -0
  5. package/dist/controls-BVX2u7sK.js +62 -0
  6. package/dist/controls-BVX2u7sK.js.map +1 -0
  7. package/dist/format-CchcJRum.js +720 -0
  8. package/dist/format-CchcJRum.js.map +1 -0
  9. package/dist/index.d.ts +28 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +35 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/items/icons.d.ts +12 -0
  14. package/dist/items/icons.d.ts.map +1 -0
  15. package/dist/items/index.d.ts +100 -0
  16. package/dist/items/index.d.ts.map +1 -0
  17. package/dist/items.js +698 -0
  18. package/dist/items.js.map +1 -0
  19. package/dist/kit/carousel.d.ts +17 -0
  20. package/dist/kit/carousel.d.ts.map +1 -0
  21. package/dist/kit/controls.d.ts +51 -0
  22. package/dist/kit/controls.d.ts.map +1 -0
  23. package/dist/kit/define.d.ts +194 -0
  24. package/dist/kit/define.d.ts.map +1 -0
  25. package/dist/kit/dom.d.ts +40 -0
  26. package/dist/kit/dom.d.ts.map +1 -0
  27. package/dist/kit/format.d.ts +49 -0
  28. package/dist/kit/format.d.ts.map +1 -0
  29. package/dist/kit/rating.d.ts +19 -0
  30. package/dist/kit/rating.d.ts.map +1 -0
  31. package/dist/kit/sparkline.d.ts +19 -0
  32. package/dist/kit/sparkline.d.ts.map +1 -0
  33. package/dist/kit/strings.d.ts +139 -0
  34. package/dist/kit/strings.d.ts.map +1 -0
  35. package/dist/link/icons.d.ts +16 -0
  36. package/dist/link/icons.d.ts.map +1 -0
  37. package/dist/link/index.d.ts +98 -0
  38. package/dist/link/index.d.ts.map +1 -0
  39. package/dist/link.js +387 -0
  40. package/dist/link.js.map +1 -0
  41. package/dist/media/icons.d.ts +28 -0
  42. package/dist/media/icons.d.ts.map +1 -0
  43. package/dist/media/index.d.ts +107 -0
  44. package/dist/media/index.d.ts.map +1 -0
  45. package/dist/media.js +610 -0
  46. package/dist/media.js.map +1 -0
  47. package/dist/order/icons.d.ts +49 -0
  48. package/dist/order/icons.d.ts.map +1 -0
  49. package/dist/order/index.d.ts +204 -0
  50. package/dist/order/index.d.ts.map +1 -0
  51. package/dist/order.js +1843 -0
  52. package/dist/order.js.map +1 -0
  53. package/dist/place/icons.d.ts +18 -0
  54. package/dist/place/icons.d.ts.map +1 -0
  55. package/dist/place/index.d.ts +207 -0
  56. package/dist/place/index.d.ts.map +1 -0
  57. package/dist/place.js +1456 -0
  58. package/dist/place.js.map +1 -0
  59. package/dist/plan/icons.d.ts +25 -0
  60. package/dist/plan/icons.d.ts.map +1 -0
  61. package/dist/plan/index.d.ts +153 -0
  62. package/dist/plan/index.d.ts.map +1 -0
  63. package/dist/plan.js +1191 -0
  64. package/dist/plan.js.map +1 -0
  65. package/dist/product/icons.d.ts +16 -0
  66. package/dist/product/icons.d.ts.map +1 -0
  67. package/dist/product/index.d.ts +133 -0
  68. package/dist/product/index.d.ts.map +1 -0
  69. package/dist/product.js +1245 -0
  70. package/dist/product.js.map +1 -0
  71. package/dist/rating-B8Mdx_b_.js +35 -0
  72. package/dist/rating-B8Mdx_b_.js.map +1 -0
  73. package/dist/score/icons.d.ts +35 -0
  74. package/dist/score/icons.d.ts.map +1 -0
  75. package/dist/score/index.d.ts +185 -0
  76. package/dist/score/index.d.ts.map +1 -0
  77. package/dist/score.js +1843 -0
  78. package/dist/score.js.map +1 -0
  79. package/dist/sparkline-C-cA9obz.js +56 -0
  80. package/dist/sparkline-C-cA9obz.js.map +1 -0
  81. package/dist/stats/icons.d.ts +19 -0
  82. package/dist/stats/icons.d.ts.map +1 -0
  83. package/dist/stats/index.d.ts +137 -0
  84. package/dist/stats/index.d.ts.map +1 -0
  85. package/dist/stats.js +1085 -0
  86. package/dist/stats.js.map +1 -0
  87. package/dist/stock/chart.d.ts +135 -0
  88. package/dist/stock/chart.d.ts.map +1 -0
  89. package/dist/stock/icons.d.ts +15 -0
  90. package/dist/stock/icons.d.ts.map +1 -0
  91. package/dist/stock/index.d.ts +195 -0
  92. package/dist/stock/index.d.ts.map +1 -0
  93. package/dist/stock.js +1013 -0
  94. package/dist/stock.js.map +1 -0
  95. package/dist/weather/conditions.d.ts +25 -0
  96. package/dist/weather/conditions.d.ts.map +1 -0
  97. package/dist/weather/icons.d.ts +12 -0
  98. package/dist/weather/icons.d.ts.map +1 -0
  99. package/dist/weather/index.d.ts +146 -0
  100. package/dist/weather/index.d.ts.map +1 -0
  101. package/dist/weather.js +1162 -0
  102. package/dist/weather.js.map +1 -0
  103. package/package.json +137 -3
package/dist/stats.js ADDED
@@ -0,0 +1,1085 @@
1
+ import { d as defineCard, o as str, p as word, b as formatNumber, e as el, t as toDate, l as intlTag, j as formatWhen, c as currencyCode, n as num, k as glyph, i as formatUnit } from "./format-CchcJRum.js";
2
+ import { s as sparkline } from "./sparkline-C-cA9obz.js";
3
+ const svg = (body) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">${body}</svg>`;
4
+ const MONEY = svg('<ellipse cx="12" cy="6.5" rx="6.5" ry="2.7"/><path d="M5.5 6.5v5c0 1.5 2.9 2.7 6.5 2.7s6.5-1.2 6.5-2.7v-5"/><path d="M5.5 11.5v5c0 1.5 2.9 2.7 6.5 2.7s6.5-1.2 6.5-2.7v-5"/>');
5
+ const USERS = svg('<circle cx="9" cy="8.5" r="3.2"/><path d="M3.2 19.2c.6-3.1 2.9-5 5.8-5s5.2 1.9 5.8 5"/><path d="M15.2 5.6a3.1 3.1 0 0 1 0 5.9"/><path d="M17.4 14.5c1.9.6 3.1 2.3 3.4 4.7"/>');
6
+ const CART = svg('<path d="M3 4h2.2l2.1 10.4a1.6 1.6 0 0 0 1.6 1.3h8a1.6 1.6 0 0 0 1.6-1.2L20.2 8H6.1"/><circle cx="9.5" cy="19.3" r="1.3"/><circle cx="17" cy="19.3" r="1.3"/>');
7
+ const CHART = svg('<path d="M4 4v15.5a.5.5 0 0 0 .5.5H20"/><path d="M7.5 15.5l3.6-4.2 3 2.6 5.1-6.4"/><path d="M16.3 7.5h2.9v2.9"/>');
8
+ const CLOCK = svg('<circle cx="12" cy="12" r="8.2"/><path d="M12 7.6V12l2.9 1.9"/>');
9
+ const STAR = svg('<path d="M12 3.6l2.5 5.2 5.7.8-4.1 4 1 5.6L12 16.5l-5.1 2.7 1-5.6-4.1-4 5.7-.8z"/>');
10
+ const STAT_ICONS = { money: MONEY, users: USERS, cart: CART, chart: CHART, clock: CLOCK, star: STAR };
11
+ const BARS = svg('<path d="M5 20V13.5"/><path d="M12 20V8.5"/><path d="M19 20V4.5"/><path d="M3 20h18"/>');
12
+ const statsStyles = `/* =========================================
13
+ The stats card — @aparte/plugin-cards/stats
14
+ =========================================
15
+ Three variants of one card, chosen by \`data-variant\` on its root:
16
+ tiles a tile per figure — label, big number, the change as a pill, a sparkline —
17
+ in a grid of two or three columns that drops to one on a narrow card.
18
+ list a fact sheet: the label at the start of each row, the value at its end.
19
+ hero the first figure large over a wide chart, the others small in a band under it.
20
+ A change is green when it went the way \`good\` says, red when not, neutral otherwise —
21
+ always with its arrow and its sign, so the colour is never the only one saying it.
22
+ Every colour is a theme token or derived from one, so light and dark follow the chat.
23
+ */
24
+
25
+ .aparte-stats-card {
26
+ --aparte-stats-pad: var(--aparte-space-6);
27
+ --aparte-stats-good: var(--aparte-success);
28
+ --aparte-stats-good-ink: var(--aparte-success-ink);
29
+ --aparte-stats-bad: var(--aparte-error);
30
+ --aparte-stats-bad-ink: var(--aparte-error-ink);
31
+ --aparte-stats-neutral: var(--aparte-text-muted);
32
+ --aparte-stats-neutral-ink: var(--aparte-text-muted);
33
+ /* A figure with no change to judge it by: its line takes the theme's accent. */
34
+ --aparte-stats-accent: var(--aparte-primary);
35
+ --aparte-stats-tile-bg: color-mix(in oklab, var(--aparte-text) 3.5%, var(--aparte-surface-1));
36
+ --aparte-stats-tile-border: color-mix(in oklab, var(--aparte-border) 75%, transparent);
37
+ --aparte-stats-tile-hover-border: color-mix(in oklab, var(--aparte-text) 18%, var(--aparte-border));
38
+ --aparte-stats-band-bg: color-mix(in oklab, var(--aparte-text) 3%, var(--aparte-surface-1));
39
+ --aparte-stats-value-size: calc(1.75rem * var(--aparte-font-scale));
40
+ --aparte-stats-hero-value-size: calc(2.75rem * var(--aparte-font-scale));
41
+ }
42
+
43
+ /* ── Widths: each variant asks for its own; the bubble's limit caps them all ── */
44
+
45
+ .aparte-stats-card[data-variant="tiles"] { --aparte-rich-card-max-inline-size: 28rem; }
46
+ .aparte-stats-card[data-variant="tiles"]:has(.aparte-stats__tiles[data-columns="1"]) { --aparte-rich-card-max-inline-size: 18rem; }
47
+ .aparte-stats-card[data-variant="tiles"]:has(.aparte-stats__tiles[data-columns="3"]) { --aparte-rich-card-max-inline-size: 36rem; }
48
+ .aparte-stats-card[data-variant="list"] { --aparte-rich-card-max-inline-size: 26rem; }
49
+ .aparte-stats-card[data-variant="hero"] { --aparte-rich-card-max-inline-size: 30rem; }
50
+
51
+ /* ── Heading and footer ──────────────────────────────────────────────── */
52
+
53
+ .aparte-stats__head {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--aparte-space-1);
57
+ padding: var(--aparte-stats-pad) var(--aparte-stats-pad) 0;
58
+ min-inline-size: 0;
59
+ }
60
+
61
+ .aparte-stats__title {
62
+ font-size: var(--aparte-font-size-base);
63
+ font-weight: var(--aparte-font-weight-semibold);
64
+ color: var(--aparte-text);
65
+ overflow-wrap: anywhere;
66
+ }
67
+
68
+ .aparte-stats__subtitle {
69
+ font-size: var(--aparte-font-size-sm);
70
+ color: var(--aparte-text-muted);
71
+ overflow-wrap: anywhere;
72
+ }
73
+
74
+ .aparte-stats__body {
75
+ /* The grid inside reads the body's width, not the window's: a card in a narrow bubble
76
+ on a wide screen still drops to fewer columns. */
77
+ container: aparte-stats / inline-size;
78
+ padding: var(--aparte-space-5) var(--aparte-stats-pad) var(--aparte-stats-pad);
79
+ min-inline-size: 0;
80
+ }
81
+
82
+ .aparte-stats__body:first-child { padding-block-start: var(--aparte-stats-pad); }
83
+
84
+ .aparte-stats__foot {
85
+ display: flex;
86
+ flex-wrap: wrap;
87
+ align-items: baseline;
88
+ justify-content: space-between;
89
+ gap: var(--aparte-space-1) var(--aparte-space-4);
90
+ padding: var(--aparte-space-3) var(--aparte-stats-pad);
91
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
92
+ color: var(--aparte-text-muted);
93
+ font-size: var(--aparte-font-size-xs);
94
+ }
95
+
96
+ .aparte-stats__footnote { overflow-wrap: anywhere; }
97
+ .aparte-stats__updated { margin-inline-start: auto; white-space: nowrap; }
98
+
99
+ /* ── A figure, whatever its shape ────────────────────────────────────── */
100
+
101
+ .aparte-stats__stat {
102
+ --aparte-rich-card-spark: var(--aparte-stats-accent);
103
+ min-inline-size: 0;
104
+ }
105
+
106
+ .aparte-stats__stat[data-tone="good"] { --aparte-rich-card-spark: var(--aparte-stats-good); }
107
+ .aparte-stats__stat[data-tone="bad"] { --aparte-rich-card-spark: var(--aparte-stats-bad); }
108
+ .aparte-stats__stat[data-tone="neutral"] { --aparte-rich-card-spark: var(--aparte-stats-neutral); }
109
+
110
+ .aparte-stats__label {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: var(--aparte-space-2);
114
+ min-inline-size: 0;
115
+ margin: 0;
116
+ color: var(--aparte-text-muted);
117
+ font-size: var(--aparte-font-size-sm);
118
+ font-weight: var(--aparte-font-weight-medium);
119
+ line-height: 1.3;
120
+ }
121
+
122
+ .aparte-stats__label-text { min-inline-size: 0; overflow-wrap: anywhere; }
123
+
124
+ /* The glyph sits on a soft chip of the accent, like an app's own KPI icons. */
125
+ .aparte-stats__icon {
126
+ --aparte-rich-card-glyph-size: 1.5rem;
127
+ box-sizing: border-box;
128
+ padding: 0.3rem;
129
+ border-radius: var(--aparte-radius-md);
130
+ background: color-mix(in oklab, var(--aparte-stats-accent) 13%, transparent);
131
+ color: var(--aparte-stats-accent);
132
+ }
133
+
134
+ .aparte-stats__value {
135
+ margin: 0;
136
+ min-inline-size: 0;
137
+ color: var(--aparte-text);
138
+ font-size: var(--aparte-stats-value-size);
139
+ font-weight: var(--aparte-font-weight-semibold);
140
+ font-variant-numeric: tabular-nums;
141
+ letter-spacing: -0.02em;
142
+ line-height: 1.1;
143
+ overflow-wrap: anywhere;
144
+ }
145
+
146
+ /* A word, not a number: a size that does not shout. */
147
+ .aparte-stats__value[data-format="text"] { letter-spacing: normal; }
148
+
149
+ .aparte-stats__hint {
150
+ color: var(--aparte-text-muted);
151
+ font-size: var(--aparte-font-size-xs);
152
+ font-weight: var(--aparte-font-weight-normal);
153
+ line-height: 1.35;
154
+ overflow-wrap: anywhere;
155
+ }
156
+
157
+ /* ── The change: a pill (arrow, signed number), then what it compares with ── */
158
+
159
+ .aparte-stats__change {
160
+ --aparte-stats-tone: var(--aparte-stats-neutral);
161
+ --aparte-stats-tone-ink: var(--aparte-stats-neutral-ink);
162
+ display: flex;
163
+ flex-wrap: wrap;
164
+ align-items: center;
165
+ gap: var(--aparte-space-1) var(--aparte-space-2);
166
+ min-inline-size: 0;
167
+ font-size: var(--aparte-font-size-xs);
168
+ line-height: 1.3;
169
+ }
170
+
171
+ .aparte-stats__change[data-tone="good"] {
172
+ --aparte-stats-tone: var(--aparte-stats-good);
173
+ --aparte-stats-tone-ink: var(--aparte-stats-good-ink);
174
+ }
175
+
176
+ .aparte-stats__change[data-tone="bad"] {
177
+ --aparte-stats-tone: var(--aparte-stats-bad);
178
+ --aparte-stats-tone-ink: var(--aparte-stats-bad-ink);
179
+ }
180
+
181
+ .aparte-stats__pill {
182
+ display: inline-flex;
183
+ align-items: center;
184
+ gap: 0.3em;
185
+ padding: 0.15em 0.55em;
186
+ border-radius: var(--aparte-radius-full);
187
+ background: color-mix(in oklab, var(--aparte-stats-tone) 14%, transparent);
188
+ color: var(--aparte-stats-tone-ink);
189
+ font-weight: var(--aparte-font-weight-semibold);
190
+ font-variant-numeric: tabular-nums;
191
+ white-space: nowrap;
192
+ }
193
+
194
+ .aparte-stats__arrow {
195
+ font-size: 0.72em;
196
+ line-height: 1;
197
+ }
198
+
199
+ /* Good or bad, as a shape: a check on a filled dot, an exclamation on a ring — told apart
200
+ without the colour. */
201
+ .aparte-stats__verdict {
202
+ display: inline-grid;
203
+ place-items: center;
204
+ inline-size: 1.15em;
205
+ block-size: 1.15em;
206
+ border-radius: 50%;
207
+ font-size: 0.78em;
208
+ font-weight: var(--aparte-font-weight-bold);
209
+ line-height: 1;
210
+ }
211
+
212
+ .aparte-stats__change[data-tone="good"] .aparte-stats__verdict {
213
+ background: var(--aparte-stats-tone-ink);
214
+ color: var(--aparte-surface-1);
215
+ }
216
+
217
+ .aparte-stats__change[data-tone="bad"] .aparte-stats__verdict {
218
+ box-shadow: inset 0 0 0 1.5px var(--aparte-stats-tone-ink);
219
+ }
220
+
221
+ .aparte-stats__change-label {
222
+ min-inline-size: 0;
223
+ color: var(--aparte-text-muted);
224
+ overflow-wrap: anywhere;
225
+ }
226
+
227
+ /* ── The sparkline ───────────────────────────────────────────────────── */
228
+
229
+ .aparte-stats__spark { min-inline-size: 0; }
230
+
231
+ /* The line draws itself in once, from where time starts. */
232
+ .aparte-stats__sparkline {
233
+ animation: aparte-stats-reveal 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
234
+ }
235
+
236
+ @keyframes aparte-stats-reveal {
237
+ from { clip-path: inset(0 100% 0 0); }
238
+ to { clip-path: inset(0 0 0 0); }
239
+ }
240
+
241
+ /* ── tiles ───────────────────────────────────────────────────────────── */
242
+
243
+ .aparte-stats__tiles {
244
+ --aparte-stats-columns: 2;
245
+ display: grid;
246
+ grid-template-columns: repeat(var(--aparte-stats-columns), minmax(0, 1fr));
247
+ gap: var(--aparte-space-3);
248
+ margin: 0;
249
+ padding: 0;
250
+ list-style: none;
251
+ }
252
+
253
+ .aparte-stats__tiles[data-columns="1"] { --aparte-stats-columns: 1; }
254
+ .aparte-stats__tiles[data-columns="3"] {
255
+ --aparte-stats-columns: 3;
256
+ --aparte-stats-value-size: calc(1.5rem * var(--aparte-font-scale));
257
+ }
258
+
259
+ .aparte-stats__tile {
260
+ display: flex;
261
+ flex-direction: column;
262
+ gap: var(--aparte-space-2);
263
+ min-inline-size: 0;
264
+ padding: var(--aparte-space-4);
265
+ border: var(--aparte-border-width) solid var(--aparte-stats-tile-border);
266
+ border-radius: var(--aparte-radius-lg);
267
+ background: var(--aparte-stats-tile-bg);
268
+ transition: border-color var(--aparte-duration-fast, 120ms) ease;
269
+ }
270
+
271
+ .aparte-stats__tile:hover { border-color: var(--aparte-stats-tile-hover-border); }
272
+
273
+ .aparte-stats__tile > .aparte-stats__value { margin-block-start: var(--aparte-space-1); }
274
+
275
+ /* Under the hint, at the tile's foot: a row of tiles keeps its charts on one line. */
276
+ .aparte-stats__tile > .aparte-stats__spark {
277
+ block-size: 2.25rem;
278
+ margin-block-start: auto;
279
+ padding-block-start: var(--aparte-space-2);
280
+ }
281
+
282
+ /* One figure alone: the card is its tile, no panel inside the panel. */
283
+ .aparte-stats__tiles[data-columns="1"] > .aparte-stats__tile {
284
+ --aparte-stats-value-size: calc(2.25rem * var(--aparte-font-scale));
285
+ padding: 0;
286
+ border: 0;
287
+ background: none;
288
+ }
289
+
290
+ .aparte-stats__tiles[data-columns="1"] > .aparte-stats__tile > .aparte-stats__spark { block-size: 3rem; }
291
+
292
+ @container aparte-stats (max-width: 30rem) {
293
+ .aparte-stats__tiles[data-columns="3"] {
294
+ --aparte-stats-columns: 2;
295
+ --aparte-stats-value-size: calc(1.75rem * var(--aparte-font-scale));
296
+ }
297
+ }
298
+
299
+ /* A phone-wide card: one tile per line, each with the whole width for its number. */
300
+ @container aparte-stats (max-width: 19.5rem) {
301
+ .aparte-stats__tiles,
302
+ .aparte-stats__tiles[data-columns="3"] { --aparte-stats-columns: 1; }
303
+ }
304
+
305
+ /* ── list ────────────────────────────────────────────────────────────── */
306
+
307
+ .aparte-stats__list {
308
+ display: flex;
309
+ flex-direction: column;
310
+ margin: 0;
311
+ }
312
+
313
+ .aparte-stats__row {
314
+ display: grid;
315
+ grid-template-columns: minmax(0, 1fr) auto;
316
+ align-items: center;
317
+ gap: var(--aparte-space-4);
318
+ padding: var(--aparte-space-3) var(--aparte-space-2);
319
+ margin-inline: calc(var(--aparte-space-2) * -1);
320
+ border-block-end: var(--aparte-border-width) solid var(--aparte-border);
321
+ border-radius: 0;
322
+ transition: background-color var(--aparte-duration-fast, 120ms) ease;
323
+ }
324
+
325
+ .aparte-stats__row:last-child { border-block-end: 0; }
326
+ .aparte-stats__row:first-child { padding-block-start: 0; }
327
+ .aparte-stats__row:last-child:not(:first-child) { padding-block-end: 0; }
328
+
329
+ .aparte-stats__list > .aparte-stats__row:hover {
330
+ background: linear-gradient(to right, transparent, color-mix(in oklab, var(--aparte-text) 3%, transparent) 30%, color-mix(in oklab, var(--aparte-text) 3%, transparent) 70%, transparent);
331
+ }
332
+
333
+ /* The label and its hint stacked, the glyph beside both. */
334
+ .aparte-stats__row > .aparte-stats__label {
335
+ display: grid;
336
+ grid-template-columns: auto minmax(0, 1fr);
337
+ align-items: center;
338
+ column-gap: var(--aparte-space-3);
339
+ row-gap: 0.1rem;
340
+ color: var(--aparte-text-muted);
341
+ font-size: var(--aparte-font-size-md);
342
+ font-weight: var(--aparte-font-weight-normal);
343
+ }
344
+
345
+ .aparte-stats__row > .aparte-stats__label:not(:has(> .aparte-stats__icon)) { grid-template-columns: minmax(0, 1fr); }
346
+ .aparte-stats__row > .aparte-stats__label > .aparte-stats__icon { grid-row: span 2; }
347
+ .aparte-stats__row > .aparte-stats__label > .aparte-stats__label-text,
348
+ .aparte-stats__row > .aparte-stats__label > .aparte-stats__hint { grid-column: -2 / -1; }
349
+
350
+ .aparte-stats__row-value {
351
+ display: flex;
352
+ flex-wrap: wrap;
353
+ align-items: center;
354
+ justify-content: flex-end;
355
+ gap: var(--aparte-space-1) var(--aparte-space-3);
356
+ margin: 0;
357
+ text-align: end;
358
+ }
359
+
360
+ .aparte-stats__row-value > .aparte-stats__value {
361
+ font-size: var(--aparte-font-size-base);
362
+ letter-spacing: normal;
363
+ line-height: 1.3;
364
+ white-space: nowrap;
365
+ }
366
+
367
+ .aparte-stats__row-value > .aparte-stats__value[data-format="text"] { white-space: normal; }
368
+
369
+ .aparte-stats__row-value > .aparte-stats__spark {
370
+ inline-size: 3.5rem;
371
+ block-size: 1.25rem;
372
+ }
373
+
374
+ .aparte-stats__row-value > .aparte-stats__change { justify-content: flex-end; }
375
+
376
+ @container aparte-stats (max-width: 19.5rem) {
377
+ .aparte-stats__row-value > .aparte-stats__spark { display: none; }
378
+ }
379
+
380
+ /* ── hero ────────────────────────────────────────────────────────────── */
381
+
382
+ .aparte-stats__hero {
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: var(--aparte-space-2);
386
+ min-inline-size: 0;
387
+ }
388
+
389
+ .aparte-stats__hero-figure {
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ align-items: center;
393
+ gap: var(--aparte-space-2) var(--aparte-space-4);
394
+ min-inline-size: 0;
395
+ }
396
+
397
+ .aparte-stats__hero .aparte-stats__value {
398
+ font-size: var(--aparte-stats-hero-value-size);
399
+ font-weight: var(--aparte-font-weight-bold);
400
+ letter-spacing: -0.03em;
401
+ line-height: 1;
402
+ }
403
+
404
+ .aparte-stats__hero .aparte-stats__change { font-size: var(--aparte-font-size-sm); }
405
+ .aparte-stats__hero .aparte-stats__hint { font-size: var(--aparte-font-size-sm); }
406
+
407
+ /* The chart runs to the card's edges, its area fading into the band below. */
408
+ .aparte-stats__hero > .aparte-stats__spark {
409
+ block-size: 5rem;
410
+ margin-block-start: var(--aparte-space-3);
411
+ margin-inline: calc(var(--aparte-stats-pad) * -1);
412
+ }
413
+
414
+ .aparte-stats__hero:last-child > .aparte-stats__spark { margin-block-end: calc(var(--aparte-stats-pad) * -1); }
415
+
416
+ .aparte-stats__hero .aparte-rich-card__sparkline-area { opacity: 0.18; }
417
+
418
+ .aparte-stats__minis {
419
+ --aparte-stats-columns: 3;
420
+ display: grid;
421
+ grid-template-columns: repeat(var(--aparte-stats-columns), minmax(0, 1fr));
422
+ gap: var(--aparte-space-4) var(--aparte-space-5);
423
+ margin: 0 calc(var(--aparte-stats-pad) * -1) calc(var(--aparte-stats-pad) * -1);
424
+ padding: var(--aparte-space-5) var(--aparte-stats-pad);
425
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
426
+ background: var(--aparte-stats-band-bg);
427
+ }
428
+
429
+ .aparte-stats__minis[data-count="1"] { --aparte-stats-columns: 1; }
430
+ .aparte-stats__minis[data-count="2"],
431
+ .aparte-stats__minis[data-count="4"] { --aparte-stats-columns: 2; }
432
+
433
+ .aparte-stats__mini {
434
+ display: flex;
435
+ flex-direction: column;
436
+ gap: var(--aparte-space-1);
437
+ min-inline-size: 0;
438
+ }
439
+
440
+ .aparte-stats__mini > .aparte-stats__label {
441
+ font-size: var(--aparte-font-size-xs);
442
+ --aparte-rich-card-glyph-size: 1.25rem;
443
+ }
444
+
445
+ .aparte-stats__mini > .aparte-stats__label > .aparte-stats__icon { padding: 0.2rem; }
446
+
447
+ .aparte-stats__mini > .aparte-stats__value {
448
+ font-size: var(--aparte-font-size-lg);
449
+ letter-spacing: -0.01em;
450
+ line-height: 1.2;
451
+ }
452
+
453
+ .aparte-stats__mini-change { margin: 0; }
454
+
455
+ .aparte-stats__mini-change .aparte-stats__pill {
456
+ padding: 0;
457
+ background: none;
458
+ }
459
+
460
+ @container aparte-stats (max-width: 19.5rem) {
461
+ .aparte-stats__minis,
462
+ .aparte-stats__minis[data-count] { --aparte-stats-columns: 2; }
463
+ .aparte-stats__minis[data-count="1"] { --aparte-stats-columns: 1; }
464
+ }
465
+
466
+ /* ── Loading ─────────────────────────────────────────────────────────── */
467
+
468
+ .aparte-stats-card[data-state="loading"] .aparte-stats__sk-title { inline-size: 9rem; }
469
+ .aparte-stats-card[data-state="loading"] .aparte-stats__sk-label { inline-size: 5rem; }
470
+ .aparte-stats-card[data-state="loading"] .aparte-stats__sk-big {
471
+ inline-size: 6.5rem;
472
+ block-size: 2rem;
473
+ border-radius: var(--aparte-radius-md);
474
+ }
475
+ .aparte-stats-card[data-state="loading"] .aparte-stats__sk-pill {
476
+ inline-size: 3.5rem;
477
+ block-size: 1.1rem;
478
+ border-radius: var(--aparte-radius-full);
479
+ }
480
+ .aparte-stats-card[data-state="loading"] .aparte-stats__sk-value { inline-size: 3.5rem; }
481
+ .aparte-stats-card[data-state="loading"] .aparte-stats__row .aparte-stats__sk-label { inline-size: 8rem; }
482
+ .aparte-stats-card[data-state="loading"] .aparte-stats__row .aparte-stats__sk-value { justify-self: end; }
483
+ .aparte-stats-card[data-state="loading"] .aparte-stats__hero .aparte-stats__sk-big { inline-size: 9rem; block-size: 2.75rem; }
484
+ .aparte-stats-card[data-state="loading"] .aparte-stats__sk-chart {
485
+ inline-size: 100%;
486
+ block-size: 4rem;
487
+ margin-block: var(--aparte-space-3) var(--aparte-space-5);
488
+ border-radius: var(--aparte-radius-md);
489
+ }
490
+ .aparte-stats-card[data-state="loading"] .aparte-stats__minis { margin-block-start: 0; }
491
+ .aparte-stats-card[data-state="loading"] .aparte-stats__mini .aparte-stats__sk-label { inline-size: 70%; }
492
+ .aparte-stats-card[data-state="loading"] .aparte-skeleton--text + .aparte-skeleton--text { margin-block-start: 0; }
493
+
494
+ @media (prefers-reduced-motion: reduce) {
495
+ .aparte-stats__sparkline { animation: none; }
496
+ }
497
+
498
+ @media (max-width: 380px) {
499
+ .aparte-stats-card { --aparte-stats-pad: var(--aparte-space-5); }
500
+ }
501
+ `;
502
+ const S = {
503
+ cardsStatsTitle: { en: "Key figures", fr: "Chiffres clés", de: "Kennzahlen", es: "Cifras clave", it: "Dati chiave", ja: "主要な数値", ko: "주요 수치", pt: "Números-chave", ru: "Ключевые показатели", zh: "关键数据" },
504
+ cardsStatsCount: { en: "{n} figures", fr: "{n} chiffres", de: "{n} Kennzahlen", es: "{n} cifras", it: "{n} dati", ja: "{n}件の数値", ko: "수치 {n}개", pt: "{n} números", ru: "Показателей: {n}", zh: "{n} 项数据" },
505
+ cardsStatsUpdated: { en: "Updated {when}", fr: "Mis à jour {when}", de: "Aktualisiert {when}", es: "Actualizado {when}", it: "Aggiornato {when}", ja: "{when} 更新", ko: "{when} 업데이트", pt: "Atualizado {when}", ru: "Обновлено {when}", zh: "更新于 {when}" },
506
+ // Said after the change ("+12% good"): the pill's green or red, for a reader who does not see colour.
507
+ cardsStatsGood: { en: "good", fr: "favorable", de: "positiv", es: "favorable", it: "positivo", ja: "好調", ko: "좋음", pt: "favorável", ru: "хорошо", zh: "向好" },
508
+ cardsStatsBad: { en: "bad", fr: "défavorable", de: "negativ", es: "desfavorable", it: "negativo", ja: "不調", ko: "나쁨", pt: "desfavorável", ru: "плохо", zh: "变差" }
509
+ };
510
+ const STATS_SHOWN = 12;
511
+ const FORMATS = ["number", "percent", "currency", "compact", "duration", "text"];
512
+ const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
513
+ function pick(r, keys) {
514
+ for (const key of keys) if (r[key] !== void 0 && r[key] !== null && r[key] !== "") return r[key];
515
+ return void 0;
516
+ }
517
+ const LABEL_KEYS = ["label", "name", "title", "key", "metric"];
518
+ const VALUE_KEYS = ["value", "amount", "count", "total"];
519
+ const LIST_KEYS = ["items", "stats", "metrics", "kpis", "figures", "facts"];
520
+ const PERCENT_TEXT = /^([+\-−]?)\s*(\d+(?:[.,]\d+)?)\s*%$/;
521
+ const GROUPED = /^[+-]?\d{1,3}(?:,\d{3})+(?:\.\d+)?$/;
522
+ function amount(value) {
523
+ const n = num(value);
524
+ if (n !== void 0) return n;
525
+ if (typeof value === "string" && GROUPED.test(value.trim())) return Number(value.trim().replace(/,/g, ""));
526
+ return void 0;
527
+ }
528
+ function percentText(value) {
529
+ if (typeof value !== "string") return void 0;
530
+ const match = PERCENT_TEXT.exec(value.trim());
531
+ if (!match) return void 0;
532
+ const n = Number(match[2].replace(",", ".")) / 100;
533
+ return match[1] === "-" || match[1] === "−" ? -n : n;
534
+ }
535
+ const asRatio = (n) => Math.abs(n) <= 1 ? n : n / 100;
536
+ function isIntlUnit(unit) {
537
+ if (!/^[a-z]+(?:-[a-z]+)*$/.test(unit)) return false;
538
+ try {
539
+ new Intl.NumberFormat("en", { style: "unit", unit });
540
+ return true;
541
+ } catch {
542
+ return false;
543
+ }
544
+ }
545
+ function readGood(r) {
546
+ const value = pick(r, ["good", "better", "goodDirection", "good_direction"]);
547
+ if (typeof value === "string") {
548
+ const v = value.trim().toLowerCase();
549
+ if (["up", "higher", "increase", "high", "more"].includes(v)) return "up";
550
+ if (["down", "lower", "decrease", "low", "less"].includes(v)) return "down";
551
+ }
552
+ const lower = pick(r, ["lowerIsBetter", "lower_is_better", "invert", "inverted"]);
553
+ if (typeof lower === "boolean") return lower ? "down" : "up";
554
+ const higher = pick(r, ["higherIsBetter", "higher_is_better"]);
555
+ if (typeof higher === "boolean") return higher ? "up" : "down";
556
+ return void 0;
557
+ }
558
+ function readTrend(value) {
559
+ if (!Array.isArray(value)) return void 0;
560
+ const points = value.map((p) => Array.isArray(p) ? num(p[1]) : isObject(p) ? num(pick(p, ["value", "y", "v", "count", "amount"])) : num(p)).filter((n) => n !== void 0).slice(-60);
561
+ return points.length >= 2 ? points : void 0;
562
+ }
563
+ function humanizeKey(key) {
564
+ const k = key.trim();
565
+ if (/\s/.test(k) || !/[a-z]/.test(k)) return k;
566
+ const words = k.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean);
567
+ const text = words.map((w) => /^[A-Z\d]{2,}$/.test(w) ? w : w.toLowerCase()).join(" ");
568
+ return text.charAt(0).toUpperCase() + text.slice(1);
569
+ }
570
+ function readStat(raw, fallbackLabel) {
571
+ if (!isObject(raw)) {
572
+ return fallbackLabel === void 0 ? null : readStat({ value: raw }, fallbackLabel);
573
+ }
574
+ const r = raw;
575
+ const label = str(pick(r, LABEL_KEYS)) ?? (fallbackLabel !== void 0 ? humanizeKey(fallbackLabel) : void 0);
576
+ const rawValue = pick(r, VALUE_KEYS);
577
+ if (!label || rawValue === void 0 || typeof rawValue !== "number" && typeof rawValue !== "string") return null;
578
+ const asked = typeof r["format"] === "string" && FORMATS.includes(r["format"]) ? r["format"] : void 0;
579
+ const currency = currencyCode(pick(r, ["currency", "currency_code", "currencyCode"]));
580
+ const unit = str(pick(r, ["unit", "units"]));
581
+ const digitsRaw = num(pick(r, ["digits", "decimals", "precision"]));
582
+ const digits = digitsRaw !== void 0 ? Math.min(6, Math.max(0, Math.round(digitsRaw))) : void 0;
583
+ let value;
584
+ let format;
585
+ let percentScale = false;
586
+ const textPercent = percentText(rawValue);
587
+ const n = amount(rawValue);
588
+ if (textPercent !== void 0 && (asked === void 0 || asked === "percent")) {
589
+ percentScale = true;
590
+ value = textPercent;
591
+ format = "percent";
592
+ } else if (n === void 0 || asked === "text") {
593
+ const text = str(rawValue);
594
+ if (!text) return null;
595
+ value = text;
596
+ format = "text";
597
+ } else if (asked === "percent") {
598
+ percentScale = Math.abs(n) > 1;
599
+ value = asRatio(n);
600
+ format = "percent";
601
+ } else {
602
+ value = n;
603
+ format = asked === "currency" ? currency ? "currency" : "number" : asked ?? (currency ? "currency" : "number");
604
+ }
605
+ const absolute = r["changeFormat"] === "absolute" || r["change_format"] === "absolute";
606
+ let change;
607
+ const percentPoints = amount(pick(r, ["changePercent", "change_percent", "percentChange", "percent_change"]));
608
+ const rawChange = pick(r, ["change", "delta", "diff", "variation"]);
609
+ const changeText = percentText(rawChange);
610
+ if (!absolute && percentPoints !== void 0) change = percentPoints / 100;
611
+ else if (changeText !== void 0) change = changeText;
612
+ else {
613
+ const c = amount(typeof rawChange === "string" ? rawChange.replace("−", "-").replace(/^\+/, "") : rawChange);
614
+ if (c !== void 0) change = absolute ? format === "percent" && percentScale ? c / 100 : c : asRatio(c);
615
+ }
616
+ const changeFormat = absolute && changeText === void 0 ? "absolute" : "percent";
617
+ const icon = typeof r["icon"] === "string" && Object.hasOwn(STAT_ICONS, r["icon"]) ? r["icon"] : void 0;
618
+ const changeLabel = str(pick(r, ["changeLabel", "change_label", "comparison", "vs", "versus"]));
619
+ const hint = str(pick(r, ["hint", "description", "note", "caption"]));
620
+ const trend = readTrend(pick(r, ["trend", "history", "series", "sparkline", "values"]));
621
+ const good = readGood(r);
622
+ return {
623
+ label,
624
+ value,
625
+ format,
626
+ ...(format === "currency" || format === "compact") && currency ? { currency } : {},
627
+ ...unit && (format === "number" || format === "compact") ? { unit } : {},
628
+ ...digits !== void 0 ? { digits } : {},
629
+ ...change !== void 0 ? { change } : {},
630
+ changeFormat,
631
+ ...change !== void 0 && changeLabel ? { changeLabel } : {},
632
+ ...good ? { good } : {},
633
+ ...trend ? { trend } : {},
634
+ ...hint ? { hint } : {},
635
+ ...icon ? { icon } : {}
636
+ };
637
+ }
638
+ const CURRENCY_KEYS = ["currency", "currency_code", "currencyCode"];
639
+ const UNIT_KEYS = ["unit", "units"];
640
+ const META_KEYS = /* @__PURE__ */ new Set([
641
+ "title",
642
+ "heading",
643
+ "subtitle",
644
+ "period",
645
+ "footnote",
646
+ "source",
647
+ "updatedAt",
648
+ "updated_at",
649
+ "asOf",
650
+ "as_of",
651
+ "lastUpdated",
652
+ ...CURRENCY_KEYS,
653
+ ...UNIT_KEYS
654
+ ]);
655
+ const FIGURE_KEYS = /* @__PURE__ */ new Set([
656
+ ...LABEL_KEYS,
657
+ ...VALUE_KEYS,
658
+ ...CURRENCY_KEYS,
659
+ ...UNIT_KEYS,
660
+ "id",
661
+ "format",
662
+ "digits",
663
+ "decimals",
664
+ "precision",
665
+ "change",
666
+ "delta",
667
+ "diff",
668
+ "variation",
669
+ "changePercent",
670
+ "change_percent",
671
+ "percentChange",
672
+ "percent_change",
673
+ "changeFormat",
674
+ "change_format",
675
+ "changeLabel",
676
+ "change_label",
677
+ "comparison",
678
+ "vs",
679
+ "versus",
680
+ "hint",
681
+ "description",
682
+ "note",
683
+ "caption",
684
+ "icon",
685
+ "trend",
686
+ "history",
687
+ "series",
688
+ "sparkline",
689
+ "values",
690
+ "good",
691
+ "better",
692
+ "goodDirection",
693
+ "good_direction",
694
+ "lowerIsBetter",
695
+ "lower_is_better",
696
+ "invert",
697
+ "inverted",
698
+ "higherIsBetter",
699
+ "higher_is_better"
700
+ ]);
701
+ function isOneFigure(r) {
702
+ if (pick(r, VALUE_KEYS) === void 0) return false;
703
+ if (pick(r, LABEL_KEYS.filter((k) => k !== "title")) !== void 0) return true;
704
+ if (pick(r, ["title"]) === void 0) return false;
705
+ const values = VALUE_KEYS.filter((k) => r[k] !== void 0 && r[k] !== null && r[k] !== "");
706
+ return values.length === 1 && Object.keys(r).every((k) => FIGURE_KEYS.has(k) || META_KEYS.has(k));
707
+ }
708
+ const MONEY_WORDS = /* @__PURE__ */ new Set([
709
+ "revenue",
710
+ "revenues",
711
+ "sales",
712
+ "income",
713
+ "profit",
714
+ "profits",
715
+ "earnings",
716
+ "cost",
717
+ "costs",
718
+ "spend",
719
+ "spending",
720
+ "price",
721
+ "prices",
722
+ "amount",
723
+ "fee",
724
+ "fees",
725
+ "budget",
726
+ "balance",
727
+ "payout",
728
+ "payouts",
729
+ "value",
730
+ "gmv",
731
+ "mrr",
732
+ "arr",
733
+ "aov",
734
+ "arpu",
735
+ "arpa",
736
+ "ltv",
737
+ "cac"
738
+ ]);
739
+ const NOT_MONEY_WORDS = /* @__PURE__ */ new Set(["count", "number", "rate", "ratio", "percent", "percentage", "share", "margin", "growth", "change"]);
740
+ function isMoneyKey(key) {
741
+ const words = humanizeKey(key).toLowerCase().split(/\s+/);
742
+ return words.some((w) => MONEY_WORDS.has(w)) && !words.some((w) => NOT_MONEY_WORDS.has(w));
743
+ }
744
+ function withRecordDefaults(value, key, meta) {
745
+ const currency = currencyCode(pick(meta, CURRENCY_KEYS));
746
+ const unit = str(pick(meta, UNIT_KEYS));
747
+ if (!currency && !unit) return value;
748
+ const figure = isObject(value) ? value : { value };
749
+ if (pick(figure, [...CURRENCY_KEYS, ...UNIT_KEYS, "format"]) !== void 0) return value;
750
+ if (currency && isMoneyKey(key)) return { ...figure, currency };
751
+ return unit ? { ...figure, unit } : value;
752
+ }
753
+ function readStats(raw) {
754
+ if (!raw || typeof raw !== "object") return null;
755
+ let stats;
756
+ let meta = {};
757
+ if (Array.isArray(raw)) {
758
+ stats = raw.map((item) => readStat(item));
759
+ } else {
760
+ const r = raw;
761
+ const list = LIST_KEYS.map((key) => r[key]).find((v) => Array.isArray(v) || isObject(v));
762
+ if (Array.isArray(list)) {
763
+ stats = list.map((item) => readStat(item));
764
+ meta = r;
765
+ } else if (isObject(list)) {
766
+ stats = Object.entries(list).map(([key, value]) => readStat(withRecordDefaults(value, key, r), key));
767
+ meta = r;
768
+ } else if (isOneFigure(r)) {
769
+ stats = [readStat(r)];
770
+ } else {
771
+ stats = Object.entries(r).filter(([key]) => !META_KEYS.has(key)).map(([key, value]) => readStat(withRecordDefaults(value, key, r), key));
772
+ meta = r;
773
+ }
774
+ }
775
+ const items = stats.filter((s) => s !== null).slice(0, STATS_SHOWN);
776
+ if (!items.length) return null;
777
+ const title = str(pick(meta, ["title", "heading"]));
778
+ const subtitle = str(pick(meta, ["subtitle", "period"]));
779
+ const footnote = str(pick(meta, ["footnote", "source"]));
780
+ const updatedAt = toDate(pick(meta, ["updatedAt", "updated_at", "asOf", "as_of", "lastUpdated"]));
781
+ return {
782
+ ...title ? { title } : {},
783
+ ...subtitle ? { subtitle } : {},
784
+ items,
785
+ ...footnote ? { footnote } : {},
786
+ ...updatedAt ? { updatedAt } : {}
787
+ };
788
+ }
789
+ const typographic = (text) => text.replace(/-/g, "−");
790
+ function durationText(config, seconds, digits) {
791
+ const s = Math.abs(seconds);
792
+ if (s < 60) return formatUnit(config, s, "second", digits ?? (s < 10 && !Number.isInteger(s) ? 1 : 0));
793
+ const units = [["day", 86400], ["hour", 3600], ["minute", 60], ["second", 1]];
794
+ let rest = Math.round(s);
795
+ const parts = [];
796
+ for (const [unit, size] of units) {
797
+ const count = Math.floor(rest / size);
798
+ rest -= count * size;
799
+ if (count > 0 || parts.length) parts.push(count > 0 ? formatUnit(config, count, unit) : "");
800
+ if (parts.length === 2) break;
801
+ }
802
+ const shown = parts.filter(Boolean);
803
+ try {
804
+ return new Intl.ListFormat(intlTag(config), { type: "unit", style: "narrow" }).format(shown);
805
+ } catch {
806
+ return shown.join(" ");
807
+ }
808
+ }
809
+ function fractionDigits(stat, value, fallback) {
810
+ if (stat.digits !== void 0) return { minimumFractionDigits: stat.digits, maximumFractionDigits: stat.digits };
811
+ return { maximumFractionDigits: Number.isInteger(value) ? 0 : fallback };
812
+ }
813
+ function writeNumber(config, stat, value, signed = false) {
814
+ const sign = signed ? { signDisplay: "exceptZero" } : {};
815
+ const done = (text) => signed ? typographic(text) : text;
816
+ switch (stat.format) {
817
+ case "percent":
818
+ return done(formatNumber(config, value, { style: "percent", ...stat.digits !== void 0 ? { minimumFractionDigits: stat.digits, maximumFractionDigits: stat.digits } : { maximumFractionDigits: 1 }, ...sign }));
819
+ case "duration": {
820
+ const text = durationText(config, value, stat.digits);
821
+ return signed && value !== 0 ? `${value > 0 ? "+" : "−"}${text}` : text;
822
+ }
823
+ case "currency":
824
+ try {
825
+ const digits = stat.digits !== void 0 || Number.isInteger(value) ? fractionDigits(stat, value, 2) : {};
826
+ return done(formatNumber(config, value, { style: "currency", currency: stat.currency, ...digits, ...sign }));
827
+ } catch {
828
+ return done(`${formatNumber(config, value, { maximumFractionDigits: 2, ...sign })} ${stat.currency}`);
829
+ }
830
+ case "compact": {
831
+ const compact = { notation: "compact", maximumFractionDigits: stat.digits ?? 1, ...sign };
832
+ if (stat.currency) {
833
+ try {
834
+ return done(formatNumber(config, value, { ...compact, style: "currency", currency: stat.currency }));
835
+ } catch {
836
+ }
837
+ }
838
+ return withUnit(config, stat, value, compact, done);
839
+ }
840
+ default:
841
+ return withUnit(config, stat, value, { ...fractionDigits(stat, value, 2), ...sign }, done);
842
+ }
843
+ }
844
+ function withUnit(config, stat, value, options, done) {
845
+ if (stat.unit && isIntlUnit(stat.unit)) {
846
+ return done(formatNumber(config, value, { ...options, style: "unit", unit: stat.unit, unitDisplay: "short" }));
847
+ }
848
+ const n = done(formatNumber(config, value, options));
849
+ return stat.unit ? `${n} ${stat.unit}` : n;
850
+ }
851
+ function statValue(config, stat) {
852
+ return typeof stat.value === "string" ? stat.value : writeNumber(config, stat, stat.value);
853
+ }
854
+ function statChange(config, stat) {
855
+ if (stat.change === void 0) return void 0;
856
+ if (stat.changeFormat === "percent") {
857
+ return typographic(formatNumber(config, stat.change, { style: "percent", maximumFractionDigits: 1, signDisplay: "exceptZero" }));
858
+ }
859
+ if (typeof stat.value === "string") {
860
+ return typographic(formatNumber(config, stat.change, { maximumFractionDigits: 2, signDisplay: "exceptZero" }));
861
+ }
862
+ return writeNumber(config, stat, stat.change, true);
863
+ }
864
+ const directionOf = (change) => change > 0 ? "up" : change < 0 ? "down" : "flat";
865
+ function toneOf(stat) {
866
+ if (stat.change === void 0) return void 0;
867
+ const direction = directionOf(stat.change);
868
+ if (!stat.good || direction === "flat") return "neutral";
869
+ return direction === stat.good ? "good" : "bad";
870
+ }
871
+ const notNull = (value) => value !== null && value !== void 0;
872
+ const ARROW = { up: "▲", down: "▼", flat: "" };
873
+ function changeOf(ctx, stat, { label = true } = {}) {
874
+ if (stat.change === void 0) return null;
875
+ const direction = directionOf(stat.change);
876
+ const delta = el("span", { class: "aparte-stats__delta" });
877
+ ctx.bind(delta, () => statChange(ctx.config, stat));
878
+ const tone = toneOf(stat);
879
+ let verdict = null;
880
+ if (tone === "good" || tone === "bad") {
881
+ const key = tone === "good" ? "cardsStatsGood" : "cardsStatsBad";
882
+ verdict = el("span", { class: "aparte-sr-only" });
883
+ ctx.bind(verdict, () => ` ${word(ctx.config, key, S[key])}`);
884
+ }
885
+ const mark = tone === "good" || tone === "bad" ? el("span", { class: "aparte-stats__verdict", attrs: { "aria-hidden": "true" }, text: tone === "good" ? "✓" : "!" }) : null;
886
+ const pill = el(
887
+ "span",
888
+ { class: "aparte-stats__pill" },
889
+ ARROW[direction] ? el("span", { class: "aparte-stats__arrow", attrs: { "aria-hidden": "true" }, text: ARROW[direction] }) : null,
890
+ delta,
891
+ mark,
892
+ verdict
893
+ );
894
+ return el(
895
+ "span",
896
+ { class: "aparte-stats__change", attrs: { "data-direction": direction, "data-tone": tone } },
897
+ pill,
898
+ label && stat.changeLabel ? el("span", { class: "aparte-stats__change-label", text: stat.changeLabel }) : null
899
+ );
900
+ }
901
+ function valueOf(ctx, stat, tag = "div") {
902
+ const node = el(tag, { class: "aparte-stats__value", attrs: { "data-format": stat.format } });
903
+ ctx.bind(node, () => statValue(ctx.config, stat));
904
+ return node;
905
+ }
906
+ function labelOf(stat, tag = "div") {
907
+ return el(
908
+ tag,
909
+ { class: "aparte-stats__label" },
910
+ stat.icon ? glyph(STAT_ICONS[stat.icon], "aparte-rich-card__glyph aparte-stats__icon") : null,
911
+ el("span", { class: "aparte-stats__label-text", text: stat.label })
912
+ );
913
+ }
914
+ function sparkOf(stat, area = false) {
915
+ if (!stat.trend) return null;
916
+ return el("div", { class: "aparte-stats__spark", attrs: { "aria-hidden": "true" } }, sparkline(stat.trend, { area, className: "aparte-stats__sparkline" }));
917
+ }
918
+ const hintOf = (stat) => stat.hint ? el("div", { class: "aparte-stats__hint", text: stat.hint }) : null;
919
+ const statAttrs = (stat) => ({ "data-tone": toneOf(stat), "data-trend": stat.trend !== void 0 });
920
+ function head(data) {
921
+ if (!data.title && !data.subtitle) return null;
922
+ return el(
923
+ "div",
924
+ { class: "aparte-stats__head" },
925
+ data.title ? el("div", { class: "aparte-stats__title", text: data.title }) : null,
926
+ data.subtitle ? el("div", { class: "aparte-stats__subtitle", text: data.subtitle }) : null
927
+ );
928
+ }
929
+ function foot(ctx, data) {
930
+ if (!data.footnote && !data.updatedAt) return null;
931
+ let when = null;
932
+ if (data.updatedAt) {
933
+ const date = data.updatedAt;
934
+ const dayOnly = date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0 && date.getMilliseconds() === 0;
935
+ when = el("time", { class: "aparte-stats__updated", attrs: { datetime: date.toISOString() } });
936
+ ctx.bind(when, () => word(ctx.config, "cardsStatsUpdated", S.cardsStatsUpdated, {
937
+ when: dayOnly ? new Intl.DateTimeFormat(intlTag(ctx.config), { weekday: "short", month: "short", day: "numeric" }).format(date) : formatWhen(ctx.config, date)
938
+ }));
939
+ }
940
+ return el(
941
+ "div",
942
+ { class: "aparte-stats__foot" },
943
+ data.footnote ? el("span", { class: "aparte-stats__footnote", text: data.footnote }) : null,
944
+ when
945
+ );
946
+ }
947
+ const columnsFor = (n) => n === 1 ? 1 : n === 2 || n === 4 ? 2 : 3;
948
+ function tiles(ctx, items) {
949
+ const list = el("ul", { class: "aparte-stats__tiles", attrs: { role: "list", "data-columns": columnsFor(items.length) } });
950
+ for (const stat of items) {
951
+ list.append(el(
952
+ "li",
953
+ { class: "aparte-stats__tile aparte-stats__stat", attrs: statAttrs(stat) },
954
+ labelOf(stat),
955
+ valueOf(ctx, stat),
956
+ changeOf(ctx, stat),
957
+ // The hint before the chart: the charts then sit on one line along a row of tiles.
958
+ hintOf(stat),
959
+ sparkOf(stat)
960
+ ));
961
+ }
962
+ return list;
963
+ }
964
+ function sheet(ctx, items) {
965
+ const list = el("dl", { class: "aparte-stats__list" });
966
+ for (const stat of items) {
967
+ const term = labelOf(stat, "dt");
968
+ if (stat.hint) term.append(el("span", { class: "aparte-stats__hint", text: stat.hint }));
969
+ const value = el(
970
+ "dd",
971
+ { class: "aparte-stats__row-value" },
972
+ sparkOf(stat),
973
+ valueOf(ctx, stat, "span"),
974
+ changeOf(ctx, stat)
975
+ );
976
+ list.append(el("div", { class: "aparte-stats__row aparte-stats__stat", attrs: statAttrs(stat) }, term, value));
977
+ }
978
+ return list;
979
+ }
980
+ function hero(ctx, items) {
981
+ const [first, ...rest] = items;
982
+ const main = el(
983
+ "div",
984
+ { class: "aparte-stats__hero aparte-stats__stat", attrs: statAttrs(first) },
985
+ labelOf(first),
986
+ el("div", { class: "aparte-stats__hero-figure" }, valueOf(ctx, first), changeOf(ctx, first)),
987
+ hintOf(first),
988
+ sparkOf(first, true)
989
+ );
990
+ if (!rest.length) return [main];
991
+ const minis = el("dl", { class: "aparte-stats__minis", attrs: { "data-count": Math.min(rest.length, 4) } });
992
+ for (const stat of rest) {
993
+ minis.append(el(
994
+ "div",
995
+ { class: "aparte-stats__mini aparte-stats__stat", attrs: statAttrs(stat) },
996
+ labelOf(stat, "dt"),
997
+ valueOf(ctx, stat, "dd"),
998
+ stat.change !== void 0 ? el("dd", { class: "aparte-stats__mini-change" }, changeOf(ctx, stat, { label: false })) : null
999
+ ));
1000
+ }
1001
+ return [main, minis];
1002
+ }
1003
+ function render(data, ctx) {
1004
+ const body = ctx.variant === "list" ? [sheet(ctx, data.items)] : ctx.variant === "hero" ? hero(ctx, data.items) : [tiles(ctx, data.items)];
1005
+ return [head(data), el("div", { class: "aparte-stats__body" }, ...body), foot(ctx, data)].filter(notNull);
1006
+ }
1007
+ function skeleton(partial, ctx) {
1008
+ const block = (cls, kind = "rect") => el("span", { class: `aparte-skeleton aparte-skeleton--${kind} ${cls}`, attrs: { "aria-hidden": "true" } });
1009
+ const heading = partial?.title ? head({ title: partial.title }) : el("div", { class: "aparte-stats__head" }, block("aparte-stats__sk-title", "text"));
1010
+ let body;
1011
+ if (ctx.variant === "list") {
1012
+ body = el("div", { class: "aparte-stats__list" }, ...[0, 1, 2, 3].map(() => el(
1013
+ "div",
1014
+ { class: "aparte-stats__row" },
1015
+ block("aparte-stats__sk-label", "text"),
1016
+ block("aparte-stats__sk-value", "text")
1017
+ )));
1018
+ } else if (ctx.variant === "hero") {
1019
+ body = el(
1020
+ "div",
1021
+ { class: "aparte-stats__sk-hero" },
1022
+ el(
1023
+ "div",
1024
+ { class: "aparte-stats__hero" },
1025
+ block("aparte-stats__sk-label", "text"),
1026
+ block("aparte-stats__sk-big"),
1027
+ block("aparte-stats__sk-chart")
1028
+ ),
1029
+ el("div", { class: "aparte-stats__minis", attrs: { "data-count": 3 } }, ...[0, 1, 2].map(() => el(
1030
+ "div",
1031
+ { class: "aparte-stats__mini" },
1032
+ block("aparte-stats__sk-label", "text"),
1033
+ block("aparte-stats__sk-value", "text")
1034
+ )))
1035
+ );
1036
+ } else {
1037
+ body = el("div", { class: "aparte-stats__tiles", attrs: { "data-columns": 2 } }, ...[0, 1, 2, 3].map(() => el(
1038
+ "div",
1039
+ { class: "aparte-stats__tile" },
1040
+ block("aparte-stats__sk-label", "text"),
1041
+ block("aparte-stats__sk-big"),
1042
+ block("aparte-stats__sk-pill")
1043
+ )));
1044
+ }
1045
+ return [heading, el("div", { class: "aparte-stats__body" }, body)].filter(notNull);
1046
+ }
1047
+ function cardLabel(data, ctx) {
1048
+ if (data?.title) return data.title;
1049
+ const items = data?.items ?? [];
1050
+ if (items.length === 1) return items[0].label;
1051
+ if (items.length > 1) return word(ctx.config, "cardsStatsCount", S.cardsStatsCount, { n: formatNumber(ctx.config, items.length) });
1052
+ return word(ctx.config, "cardsStatsTitle", S.cardsStatsTitle);
1053
+ }
1054
+ const statsCard = defineCard({
1055
+ name: "stats",
1056
+ variants: ["tiles", "list", "hero"],
1057
+ styles: statsStyles,
1058
+ icon: BARS,
1059
+ normalize: readStats,
1060
+ render,
1061
+ skeleton,
1062
+ label: cardLabel,
1063
+ // "Revenue: $12,000 (+12% vs last week); Orders: 340" — one clause per figure.
1064
+ fallback: (data, ctx) => data.items.map((stat) => {
1065
+ const change = statChange(ctx.config, stat);
1066
+ const said = change ? ` (${[change, stat.changeLabel].filter(notNull).join(" ")})` : "";
1067
+ return `${stat.label}: ${statValue(ctx.config, stat)}${said}`;
1068
+ }).join("; "),
1069
+ partial: (input) => {
1070
+ const about = str(input["title"] ?? input["metric"] ?? input["metrics"] ?? input["query"] ?? input["topic"] ?? input["report"]);
1071
+ return about ? { title: about } : null;
1072
+ }
1073
+ });
1074
+ function setupStatsCard(options = {}) {
1075
+ return statsCard.setup(options);
1076
+ }
1077
+ export {
1078
+ humanizeKey,
1079
+ readStats,
1080
+ setupStatsCard,
1081
+ statChange,
1082
+ statValue,
1083
+ statsCard
1084
+ };
1085
+ //# sourceMappingURL=stats.js.map