@alfadocs/ui-kit-debug 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/carousel.agent-2dXpQoqp.js +1661 -0
- package/dist/_chunks/carousel.agent-2dXpQoqp.js.map +1 -0
- package/dist/_chunks/{header-D0ULgQl3.js → header-DqmKROIY.js} +42 -52
- package/dist/_chunks/header-DqmKROIY.js.map +1 -0
- package/dist/_chunks/{map-view-Dd48BxVB.js → map-view-DVP-Kp9l.js} +445 -444
- package/dist/_chunks/{map-view-Dd48BxVB.js.map → map-view-DVP-Kp9l.js.map} +1 -1
- package/dist/_chunks/menu-XRhW3_99.js +16 -0
- package/dist/_chunks/menu-XRhW3_99.js.map +1 -0
- package/dist/_chunks/{patient-shell-CL20JnVJ.js → patient-shell-BE0CdPOJ.js} +2 -2
- package/dist/_chunks/{patient-shell-CL20JnVJ.js.map → patient-shell-BE0CdPOJ.js.map} +1 -1
- package/dist/_chunks/public-header.agent-AzJSINlU.js +237 -0
- package/dist/_chunks/public-header.agent-AzJSINlU.js.map +1 -0
- package/dist/_chunks/stat-DEkZx0Mx.js +318 -0
- package/dist/_chunks/stat-DEkZx0Mx.js.map +1 -0
- package/dist/_chunks/use-count-up-BLLetaZ8.js +109 -0
- package/dist/_chunks/use-count-up-BLLetaZ8.js.map +1 -0
- package/dist/agent-catalog.json +92 -1
- package/dist/components/carousel/carousel.agent.d.ts +4 -0
- package/dist/components/carousel/carousel.agent.d.ts.map +1 -0
- package/dist/components/carousel/carousel.d.ts +45 -0
- package/dist/components/carousel/carousel.d.ts.map +1 -0
- package/dist/components/carousel/index.d.ts +3 -0
- package/dist/components/carousel/index.d.ts.map +1 -0
- package/dist/components/carousel/index.js +6 -0
- package/dist/components/carousel/index.js.map +1 -0
- package/dist/components/header/index.js +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/map-view/index.js +1 -1
- package/dist/components/map-view/map-view.d.ts +8 -0
- package/dist/components/map-view/map-view.d.ts.map +1 -1
- package/dist/components/public-header/index.d.ts +3 -0
- package/dist/components/public-header/index.d.ts.map +1 -0
- package/dist/components/public-header/index.js +6 -0
- package/dist/components/public-header/index.js.map +1 -0
- package/dist/components/public-header/public-header.agent.d.ts +4 -0
- package/dist/components/public-header/public-header.agent.d.ts.map +1 -0
- package/dist/components/public-header/public-header.d.ts +43 -0
- package/dist/components/public-header/public-header.d.ts.map +1 -0
- package/dist/components/stat/index.js +1 -1
- package/dist/components/stat/stat.d.ts +31 -0
- package/dist/components/stat/stat.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +10 -8
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/use-count-up.d.ts +88 -0
- package/dist/hooks/use-count-up.d.ts.map +1 -0
- package/dist/i18n/config.js +51 -0
- package/dist/i18n/config.js.map +1 -1
- package/dist/i18n/resources.d.ts +51 -0
- package/dist/i18n/resources.d.ts.map +1 -1
- package/dist/index.js +367 -361
- package/dist/index.js.map +1 -1
- package/dist/locales/de.json +17 -0
- package/dist/locales/en.json +17 -0
- package/dist/locales/it.json +17 -0
- package/dist/patterns/patient-shell/index.js +1 -1
- package/dist/tokens.css +1 -1
- package/package.json +10 -1
- package/dist/_chunks/header-D0ULgQl3.js.map +0 -1
- package/dist/_chunks/stat-CDQ_a0vk.js +0 -228
- package/dist/_chunks/stat-CDQ_a0vk.js.map +0 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { jsxs as z, jsx as i } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as Q, useId as X, useCallback as L, useRef as Z, useMemo as tt } from "react";
|
|
3
|
+
import { c as S } from "./index-D2ZczOXr.js";
|
|
4
|
+
import { useTranslation as U } from "react-i18next";
|
|
5
|
+
import { u as et } from "./use-count-up-BLLetaZ8.js";
|
|
6
|
+
const O = S("ds:flex ds:flex-col", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
default: "ds:gap-[var(--spacing-xs)]",
|
|
10
|
+
outlined: [
|
|
11
|
+
"ds:gap-[var(--spacing-xs)]",
|
|
12
|
+
"ds:border ds:border-[color:var(--border)]",
|
|
13
|
+
"ds:rounded-[var(--radius-md)]",
|
|
14
|
+
"ds:ps-[var(--spacing-md)] ds:pe-[var(--spacing-md)]",
|
|
15
|
+
"ds:pt-[var(--spacing-md)] ds:pb-[var(--spacing-md)]"
|
|
16
|
+
].join(" "),
|
|
17
|
+
// White card surface with the shared card shadow — matches
|
|
18
|
+
// `Card variant="elevated"` tokens (`--card` + `--shadow-card`) so
|
|
19
|
+
// a grid of elevated Stats reads as a set of shaded panels without
|
|
20
|
+
// nesting a Card around each one.
|
|
21
|
+
elevated: [
|
|
22
|
+
"ds:gap-[var(--spacing-xs)]",
|
|
23
|
+
"ds:bg-[var(--card)]",
|
|
24
|
+
"ds:text-[var(--card-foreground)]",
|
|
25
|
+
"ds:shadow-[var(--shadow-card)]",
|
|
26
|
+
"ds:rounded-[var(--radius-lg)]",
|
|
27
|
+
"ds:ps-[var(--spacing-md)] ds:pe-[var(--spacing-md)]",
|
|
28
|
+
"ds:pt-[var(--spacing-md)] ds:pb-[var(--spacing-md)]",
|
|
29
|
+
// Forced-colors: UA strips shadow, so render a border as the
|
|
30
|
+
// separation cue — mirrors Card's forced-colors behaviour.
|
|
31
|
+
"ds:forced-colors:border ds:forced-colors:border-[CanvasText]"
|
|
32
|
+
].join(" "),
|
|
33
|
+
compact: "ds:gap-[var(--spacing-none)]"
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
sm: "",
|
|
37
|
+
md: "",
|
|
38
|
+
lg: ""
|
|
39
|
+
},
|
|
40
|
+
align: {
|
|
41
|
+
start: "ds:items-start ds:text-start",
|
|
42
|
+
center: "ds:items-center ds:text-center",
|
|
43
|
+
end: "ds:items-end ds:text-end"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
defaultVariants: {
|
|
47
|
+
variant: "default",
|
|
48
|
+
size: "md",
|
|
49
|
+
align: "start"
|
|
50
|
+
}
|
|
51
|
+
}), P = S("type-metric ds:text-[var(--foreground)]", {
|
|
52
|
+
variants: {
|
|
53
|
+
size: {
|
|
54
|
+
sm: "ds:[--type-metric-size:var(--font-size-xl)]",
|
|
55
|
+
md: "ds:[--type-metric-size:var(--font-size-3xl)]",
|
|
56
|
+
lg: "ds:[--type-metric-size:var(--font-size-5xl)]"
|
|
57
|
+
},
|
|
58
|
+
variant: {
|
|
59
|
+
default: "",
|
|
60
|
+
outlined: "",
|
|
61
|
+
elevated: "",
|
|
62
|
+
compact: ""
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
compoundVariants: [
|
|
66
|
+
{
|
|
67
|
+
variant: "compact",
|
|
68
|
+
size: "sm",
|
|
69
|
+
className: "ds:[--type-metric-size:var(--font-size-lg)]"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
variant: "compact",
|
|
73
|
+
size: "md",
|
|
74
|
+
className: "ds:[--type-metric-size:var(--font-size-xl)]"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
variant: "compact",
|
|
78
|
+
size: "lg",
|
|
79
|
+
className: "ds:[--type-metric-size:var(--font-size-2xl)]"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
defaultVariants: {
|
|
83
|
+
size: "md",
|
|
84
|
+
variant: "default"
|
|
85
|
+
}
|
|
86
|
+
}), st = S(
|
|
87
|
+
"ds:inline-flex ds:items-center ds:gap-[var(--spacing-xs)] type-label",
|
|
88
|
+
{
|
|
89
|
+
variants: {
|
|
90
|
+
trend: {
|
|
91
|
+
up: "ds:text-[var(--success)]",
|
|
92
|
+
down: "ds:text-[var(--destructive)]",
|
|
93
|
+
flat: "ds:text-[var(--muted-foreground)]"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
defaultVariants: {
|
|
97
|
+
trend: "flat"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
), rt = { up: "↑", down: "↓", flat: "→" }, at = {
|
|
101
|
+
up: "stat.trend.up",
|
|
102
|
+
down: "stat.trend.down",
|
|
103
|
+
flat: "stat.trend.flat"
|
|
104
|
+
};
|
|
105
|
+
function nt({
|
|
106
|
+
trend: d,
|
|
107
|
+
delta: t,
|
|
108
|
+
deltaFormat: c = "decimal",
|
|
109
|
+
locale: a
|
|
110
|
+
}) {
|
|
111
|
+
const { t: e, i18n: o } = U(), l = typeof t == "string" ? t : new Intl.NumberFormat(a ?? o.language, {
|
|
112
|
+
style: c === "percent" ? "percent" : "decimal"
|
|
113
|
+
}).format(c === "percent" ? t / 100 : t);
|
|
114
|
+
return /* @__PURE__ */ z("span", { className: st({ trend: d }), children: [
|
|
115
|
+
/* @__PURE__ */ i("span", { "aria-hidden": "true", children: rt[d] }),
|
|
116
|
+
/* @__PURE__ */ i("span", { className: "ds:sr-only", children: e(at[d]) }),
|
|
117
|
+
l
|
|
118
|
+
] });
|
|
119
|
+
}
|
|
120
|
+
function it(d, t) {
|
|
121
|
+
const c = d.trim();
|
|
122
|
+
if (c === "") return null;
|
|
123
|
+
let a = "", e = c;
|
|
124
|
+
e.startsWith("+") ? (a = "+", e = e.slice(1).trimStart()) : (e.startsWith("-") || e.startsWith("−")) && (a = "-", e = e.slice(1).trimStart());
|
|
125
|
+
let o = "";
|
|
126
|
+
const l = e.match(/[KMB]$/i);
|
|
127
|
+
l && (o = l[0], e = e.slice(0, -1).trimEnd());
|
|
128
|
+
let m = "", u = ".";
|
|
129
|
+
try {
|
|
130
|
+
const h = new Intl.NumberFormat(t).formatToParts(12345.6);
|
|
131
|
+
for (const n of h)
|
|
132
|
+
n.type === "group" ? m = n.value : n.type === "decimal" && (u = n.value);
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
const v = e.match(/^(\d+)\.(\d+)$/);
|
|
136
|
+
if (v && !(v[2].length === 3 && m === ".")) {
|
|
137
|
+
const n = Number.parseFloat(e);
|
|
138
|
+
if (Number.isFinite(n))
|
|
139
|
+
return { target: n, sign: a, suffix: o };
|
|
140
|
+
}
|
|
141
|
+
let s = e;
|
|
142
|
+
m !== "" && (s = s.split(m).join("")), u !== "." && (s = s.split(u).join(".")), s = s.replace(/[\s\u00A0\u202F]/g, "");
|
|
143
|
+
const b = Number.parseFloat(s);
|
|
144
|
+
return Number.isFinite(b) ? { target: b, sign: a, suffix: o } : null;
|
|
145
|
+
}
|
|
146
|
+
const dt = Q(
|
|
147
|
+
({
|
|
148
|
+
label: d,
|
|
149
|
+
value: t,
|
|
150
|
+
format: c,
|
|
151
|
+
currency: a,
|
|
152
|
+
locale: e,
|
|
153
|
+
trend: o,
|
|
154
|
+
delta: l,
|
|
155
|
+
deltaFormat: m = "decimal",
|
|
156
|
+
icon: u,
|
|
157
|
+
loading: v = !1,
|
|
158
|
+
animate: s = !1,
|
|
159
|
+
animateOnEveryView: b = !1,
|
|
160
|
+
animateDurationMs: h = 1600,
|
|
161
|
+
animateLocale: n,
|
|
162
|
+
animateSeparators: p,
|
|
163
|
+
variant: x = "default",
|
|
164
|
+
size: y = "md",
|
|
165
|
+
align: I = "start",
|
|
166
|
+
className: M,
|
|
167
|
+
...j
|
|
168
|
+
}, k) => {
|
|
169
|
+
const { i18n: B } = U(), R = X(), f = e ?? B.language, K = L(() => {
|
|
170
|
+
if (typeof t == "string") return t;
|
|
171
|
+
switch (c) {
|
|
172
|
+
case "currency":
|
|
173
|
+
return new Intl.NumberFormat(f, {
|
|
174
|
+
style: "currency",
|
|
175
|
+
currency: a ?? "USD"
|
|
176
|
+
}).format(t);
|
|
177
|
+
case "percent":
|
|
178
|
+
return new Intl.NumberFormat(f, { style: "percent" }).format(
|
|
179
|
+
t
|
|
180
|
+
);
|
|
181
|
+
case "decimal":
|
|
182
|
+
return new Intl.NumberFormat(f, { style: "decimal" }).format(
|
|
183
|
+
t
|
|
184
|
+
);
|
|
185
|
+
default:
|
|
186
|
+
return new Intl.NumberFormat(f).format(t);
|
|
187
|
+
}
|
|
188
|
+
}, [t, c, f, a])(), N = n ?? f;
|
|
189
|
+
Z(!1);
|
|
190
|
+
const r = tt(() => s ? typeof t == "number" ? Number.isFinite(t) ? { target: t, sign: "", suffix: "" } : null : it(t, N) : null, [s, t, N]), Y = s && r !== null, _ = (r == null ? void 0 : r.target) ?? 0, F = (r == null ? void 0 : r.sign) ?? "", T = (r == null ? void 0 : r.suffix) ?? "", q = L(
|
|
191
|
+
(w) => {
|
|
192
|
+
const C = (() => {
|
|
193
|
+
try {
|
|
194
|
+
return new Intl.NumberFormat(
|
|
195
|
+
N,
|
|
196
|
+
p ? { useGrouping: !0 } : void 0
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
let g;
|
|
203
|
+
if (C)
|
|
204
|
+
if (p) {
|
|
205
|
+
const W = Math.abs(w), A = Math.trunc(W), D = W - A, H = p.thousand ?? "", V = String(A).replace(
|
|
206
|
+
/\B(?=(\d{3})+(?!\d))/g,
|
|
207
|
+
H
|
|
208
|
+
);
|
|
209
|
+
if (D === 0)
|
|
210
|
+
g = V;
|
|
211
|
+
else {
|
|
212
|
+
const G = D.toFixed(6).replace(/0+$/, "").slice(2), J = p.decimal ?? ".";
|
|
213
|
+
g = G ? V + J + G : V;
|
|
214
|
+
}
|
|
215
|
+
} else
|
|
216
|
+
g = C.format(Math.abs(w));
|
|
217
|
+
else
|
|
218
|
+
g = String(Math.abs(w));
|
|
219
|
+
let E = F;
|
|
220
|
+
return F === "" && w < 0 && (E = "-"), `${E}${g}${T}`;
|
|
221
|
+
},
|
|
222
|
+
[N, p, F, T]
|
|
223
|
+
), $ = et({
|
|
224
|
+
to: _,
|
|
225
|
+
from: 0,
|
|
226
|
+
durationMs: h,
|
|
227
|
+
startOnVisible: !0,
|
|
228
|
+
retriggerOnReEntry: b,
|
|
229
|
+
// `formatter` is the only path we use — it owns both the locale
|
|
230
|
+
// formatting and the sign/suffix re-application.
|
|
231
|
+
formatter: q
|
|
232
|
+
});
|
|
233
|
+
return v ? /* @__PURE__ */ z(
|
|
234
|
+
"div",
|
|
235
|
+
{
|
|
236
|
+
ref: k,
|
|
237
|
+
role: "group",
|
|
238
|
+
"aria-busy": "true",
|
|
239
|
+
"aria-label": d,
|
|
240
|
+
"data-component": "stat",
|
|
241
|
+
className: O({ variant: x, size: y, align: I, className: M }),
|
|
242
|
+
...j,
|
|
243
|
+
children: [
|
|
244
|
+
/* @__PURE__ */ i("div", { className: "ds:h-[var(--skeleton-label-h)] ds:w-[var(--skeleton-label-w)] ds:animate-pulse ds:rounded-[var(--radius-sm)] ds:bg-[var(--muted)]" }),
|
|
245
|
+
/* @__PURE__ */ i("div", { className: "ds:h-[var(--skeleton-value-h)] ds:w-[var(--skeleton-value-w)] ds:animate-pulse ds:rounded-[var(--radius-sm)] ds:bg-[var(--muted)]" })
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
) : /* @__PURE__ */ z(
|
|
249
|
+
"div",
|
|
250
|
+
{
|
|
251
|
+
ref: k,
|
|
252
|
+
role: "group",
|
|
253
|
+
"aria-labelledby": R,
|
|
254
|
+
"data-component": "stat",
|
|
255
|
+
className: O({ variant: x, size: y, align: I, className: M }),
|
|
256
|
+
...j,
|
|
257
|
+
children: [
|
|
258
|
+
/* @__PURE__ */ z("span", { className: "ds:inline-flex ds:items-center", children: [
|
|
259
|
+
u && /* @__PURE__ */ i(
|
|
260
|
+
"span",
|
|
261
|
+
{
|
|
262
|
+
"aria-hidden": "true",
|
|
263
|
+
className: [
|
|
264
|
+
"ds:me-[var(--spacing-xs)] ds:inline-flex ds:shrink-0",
|
|
265
|
+
// Accent tint on the leading icon. The icon is decorative
|
|
266
|
+
// (aria-hidden), so magenta-500's 3.2:1 contrast on white
|
|
267
|
+
// is acceptable here — the Stat label + value still
|
|
268
|
+
// carry the accessible name at `--foreground`.
|
|
269
|
+
"ds:text-[color:var(--accent)]",
|
|
270
|
+
"ds:forced-colors:text-[CanvasText]"
|
|
271
|
+
].join(" "),
|
|
272
|
+
children: u
|
|
273
|
+
}
|
|
274
|
+
),
|
|
275
|
+
/* @__PURE__ */ i(
|
|
276
|
+
"span",
|
|
277
|
+
{
|
|
278
|
+
id: R,
|
|
279
|
+
className: "type-label ds:text-[var(--muted-foreground)]",
|
|
280
|
+
children: d
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
] }),
|
|
284
|
+
Y ? (
|
|
285
|
+
// The animated number is wrapped in an `aria-live="off"` span
|
|
286
|
+
// so screen readers don't announce every interpolated frame.
|
|
287
|
+
// The `role="group"` ancestor (with `aria-labelledby` pointing
|
|
288
|
+
// at the label) carries the final accessible reading — the
|
|
289
|
+
// animated frames are decorative only.
|
|
290
|
+
/* @__PURE__ */ i(
|
|
291
|
+
"span",
|
|
292
|
+
{
|
|
293
|
+
ref: $.ref,
|
|
294
|
+
"aria-live": "off",
|
|
295
|
+
className: P({ size: y, variant: x }),
|
|
296
|
+
children: $.value
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
) : /* @__PURE__ */ i("span", { className: P({ size: y, variant: x }), children: K }),
|
|
300
|
+
o != null && l != null && /* @__PURE__ */ i(
|
|
301
|
+
nt,
|
|
302
|
+
{
|
|
303
|
+
trend: o,
|
|
304
|
+
delta: l,
|
|
305
|
+
deltaFormat: m,
|
|
306
|
+
locale: e
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
dt.displayName = "Stat";
|
|
315
|
+
export {
|
|
316
|
+
dt as S
|
|
317
|
+
};
|
|
318
|
+
//# sourceMappingURL=stat-DEkZx0Mx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stat-DEkZx0Mx.js","sources":["../../src/components/stat/stat.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useId,\n useMemo,\n useRef,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { useTranslation } from 'react-i18next';\nimport { useCountUp } from '../../hooks/use-count-up';\n\n/* ------------------------------------------------------------------ */\n/* CVA — stat root */\n/* ------------------------------------------------------------------ */\n\nconst statVariants = cva('ds:flex ds:flex-col', {\n variants: {\n variant: {\n default: 'ds:gap-[var(--spacing-xs)]',\n outlined: [\n 'ds:gap-[var(--spacing-xs)]',\n 'ds:border ds:border-[color:var(--border)]',\n 'ds:rounded-[var(--radius-md)]',\n 'ds:ps-[var(--spacing-md)] ds:pe-[var(--spacing-md)]',\n 'ds:pt-[var(--spacing-md)] ds:pb-[var(--spacing-md)]',\n ].join(' '),\n // White card surface with the shared card shadow — matches\n // `Card variant=\"elevated\"` tokens (`--card` + `--shadow-card`) so\n // a grid of elevated Stats reads as a set of shaded panels without\n // nesting a Card around each one.\n elevated: [\n 'ds:gap-[var(--spacing-xs)]',\n 'ds:bg-[var(--card)]',\n 'ds:text-[var(--card-foreground)]',\n 'ds:shadow-[var(--shadow-card)]',\n 'ds:rounded-[var(--radius-lg)]',\n 'ds:ps-[var(--spacing-md)] ds:pe-[var(--spacing-md)]',\n 'ds:pt-[var(--spacing-md)] ds:pb-[var(--spacing-md)]',\n // Forced-colors: UA strips shadow, so render a border as the\n // separation cue — mirrors Card's forced-colors behaviour.\n 'ds:forced-colors:border ds:forced-colors:border-[CanvasText]',\n ].join(' '),\n compact: 'ds:gap-[var(--spacing-none)]',\n },\n size: {\n sm: '',\n md: '',\n lg: '',\n },\n align: {\n start: 'ds:items-start ds:text-start',\n center: 'ds:items-center ds:text-center',\n end: 'ds:items-end ds:text-end',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'md',\n align: 'start',\n },\n});\n\n/* ------------------------------------------------------------------ */\n/* CVA — value element */\n/* ------------------------------------------------------------------ */\n\nconst valueVariants = cva('type-metric ds:text-[var(--foreground)]', {\n variants: {\n size: {\n sm: 'ds:[--type-metric-size:var(--font-size-xl)]',\n md: 'ds:[--type-metric-size:var(--font-size-3xl)]',\n lg: 'ds:[--type-metric-size:var(--font-size-5xl)]',\n },\n variant: {\n default: '',\n outlined: '',\n elevated: '',\n compact: '',\n },\n },\n compoundVariants: [\n {\n variant: 'compact',\n size: 'sm',\n className: 'ds:[--type-metric-size:var(--font-size-lg)]',\n },\n {\n variant: 'compact',\n size: 'md',\n className: 'ds:[--type-metric-size:var(--font-size-xl)]',\n },\n {\n variant: 'compact',\n size: 'lg',\n className: 'ds:[--type-metric-size:var(--font-size-2xl)]',\n },\n ],\n defaultVariants: {\n size: 'md',\n variant: 'default',\n },\n});\n\n/* ------------------------------------------------------------------ */\n/* CVA — trend element */\n/* ------------------------------------------------------------------ */\n\nconst trendVariants = cva(\n 'ds:inline-flex ds:items-center ds:gap-[var(--spacing-xs)] type-label',\n {\n variants: {\n trend: {\n up: 'ds:text-[var(--success)]',\n down: 'ds:text-[var(--destructive)]',\n flat: 'ds:text-[var(--muted-foreground)]',\n },\n },\n defaultVariants: {\n trend: 'flat',\n },\n },\n);\n\n/* ------------------------------------------------------------------ */\n/* StatTrend (internal — not exported) */\n/* ------------------------------------------------------------------ */\n\ninterface StatTrendProps {\n trend: 'up' | 'down' | 'flat';\n delta: number | string;\n deltaFormat?: 'decimal' | 'percent';\n locale?: string;\n}\n\nconst ARROW = { up: '↑', down: '↓', flat: '→' } as const;\nconst TREND_KEY = {\n up: 'stat.trend.up',\n down: 'stat.trend.down',\n flat: 'stat.trend.flat',\n} as const;\n\nfunction StatTrend({\n trend,\n delta,\n deltaFormat = 'decimal',\n locale,\n}: StatTrendProps) {\n const { t, i18n } = useTranslation();\n\n const formattedDelta =\n typeof delta === 'string'\n ? delta\n : new Intl.NumberFormat(locale ?? i18n.language, {\n style: deltaFormat === 'percent' ? 'percent' : 'decimal',\n }).format(deltaFormat === 'percent' ? delta / 100 : delta);\n\n return (\n <span className={trendVariants({ trend })}>\n <span aria-hidden=\"true\">{ARROW[trend]}</span>\n <span className=\"ds:sr-only\">{t(TREND_KEY[trend])}</span>\n {formattedDelta}\n </span>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* Parse a string value into a numeric target for the count-up */\n/* ------------------------------------------------------------------ */\n\ntype ParsedValue = {\n /** Numeric target for the count-up. */\n target: number;\n /** Leading sign captured from the input string (e.g. \"+\" / \"-\"). */\n sign: '' | '+' | '-';\n /** Trailing shorthand suffix (e.g. \"K\" / \"M\" / \"B\") to re-append. */\n suffix: string;\n};\n\n/**\n * Strip locale-aware separators, a leading sign, and the K/M/B\n * shorthand suffix from a pre-formatted string so the count-up has a\n * pure numeric target to animate towards. Returns `null` for any input\n * that can't be parsed — the caller short-circuits to static rendering\n * in that case.\n */\nfunction parseStatValue(raw: string, locale: string): ParsedValue | null {\n const trimmed = raw.trim();\n if (trimmed === '') return null;\n\n // Capture leading sign so we can re-attach it every frame. Stat's\n // marketing usage (\"+500.000\", \"-12\") often relies on it.\n let sign: '' | '+' | '-' = '';\n let body = trimmed;\n if (body.startsWith('+')) {\n sign = '+';\n body = body.slice(1).trimStart();\n } else if (body.startsWith('-') || body.startsWith('−')) {\n // Hyphen-minus or the typographic minus sign — both treated as -.\n sign = '-';\n body = body.slice(1).trimStart();\n }\n\n // Trailing K / M / B shorthand (case-insensitive). Re-append verbatim\n // so \"+1.5K\" renders as \"+1.5K\" at every frame.\n let suffix = '';\n const suffixMatch = body.match(/[KMB]$/i);\n if (suffixMatch) {\n suffix = suffixMatch[0];\n body = body.slice(0, -1).trimEnd();\n }\n\n // Discover the locale's group + decimal glyphs from a probe value.\n // formatToParts is the canonical way; we fall back to `.` decimal /\n // `,` group if Intl can't tell us (no-locale envs).\n let group = '';\n let decimal = '.';\n try {\n const probe = new Intl.NumberFormat(locale).formatToParts(12345.6);\n for (const part of probe) {\n if (part.type === 'group') group = part.value;\n else if (part.type === 'decimal') decimal = part.value;\n }\n } catch {\n /* swallow — defaults already set */\n }\n\n // Heuristic for input strings that look like JS-native decimals\n // (e.g. `\"1500.25\"`) regardless of the requested locale: if the body\n // is a clean US-style decimal AND the fraction after the `.` is NOT\n // a three-digit thousands group in a locale that uses `.` as the\n // group separator, treat it as a direct numeric literal. This covers\n // the common case where a caller types `value=\"1500.25\"` and expects\n // the count-up to land on 1500.25 even though the output locale is\n // it-IT (which uses `.` for thousands).\n const usStyleMatch = body.match(/^(\\d+)\\.(\\d+)$/);\n if (usStyleMatch) {\n const fractionLen = usStyleMatch[2].length;\n if (!(fractionLen === 3 && group === '.')) {\n const direct = Number.parseFloat(body);\n if (Number.isFinite(direct)) {\n return { target: direct, sign, suffix };\n }\n }\n }\n\n // Strip every group separator we discovered, then normalise the\n // decimal separator to `.` so JS can parseFloat the result.\n // Note: when the locale's group separator is empty (e.g. some\n // numbering systems) the replace is a no-op.\n let normalised = body;\n if (group !== '') {\n normalised = normalised.split(group).join('');\n }\n if (decimal !== '.') {\n normalised = normalised.split(decimal).join('.');\n }\n\n // Defence-in-depth: strip whitespace plus NBSP (U+00A0) and NARROW\n // NO-BREAK SPACE (U+202F) that some locales (fr-FR, it-IT) use as\n // thousand separators. Hex escapes keep this source ASCII-clean.\n normalised = normalised.replace(/[\\s\\u00A0\\u202F]/g, '');\n\n const numeric = Number.parseFloat(normalised);\n if (!Number.isFinite(numeric)) return null;\n\n return { target: numeric, sign, suffix };\n}\n\n/* ------------------------------------------------------------------ */\n/* StatProps */\n/* ------------------------------------------------------------------ */\n\nexport interface StatProps\n extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof statVariants> {\n /** Metric label rendered above the value. */\n label: string;\n /** Numeric or pre-formatted string value. */\n value: number | string;\n /** Number formatting style. Omit to use default decimal formatting. */\n format?: 'decimal' | 'currency' | 'percent';\n /** ISO 4217 currency code — required when format=\"currency\". */\n currency?: string;\n /** BCP 47 locale override. Defaults to the active i18n language. */\n locale?: string;\n /** Trend direction. Requires `delta` to render the trend row. */\n trend?: 'up' | 'down' | 'flat';\n /** Trend delta magnitude, e.g. 12 for \"↑ 12%\". */\n delta?: number | string;\n /** Number formatting style for the delta. Default \"decimal\". */\n deltaFormat?: 'decimal' | 'percent';\n /** Optional leading icon rendered at the start of the label row. */\n icon?: ReactNode;\n /** Renders animated skeleton placeholders instead of content. */\n loading?: boolean;\n /**\n * Animate the numeric value from 0 (or from the captured leading\n * sign) to the final value when the Stat first scrolls into view.\n * Default `false`. Marketing surfaces opt in; dashboards leave it\n * off so live metrics render immediately.\n */\n animate?: boolean;\n /**\n * When `true`, the count-up re-plays every time the Stat scrolls\n * out of view and back in. Default `false` — animate once per\n * element per session, matching the booking-website hero pattern.\n * Ignored under `prefers-reduced-motion: reduce`.\n */\n animateOnEveryView?: boolean;\n /** Animation duration in milliseconds. Default 1600. */\n animateDurationMs?: number;\n /**\n * BCP 47 locale used for the per-frame count-up formatting. Falls\n * back to `locale`, then to the active i18n language.\n */\n animateLocale?: string;\n /**\n * Override the locale's thousand / decimal glyphs for the count-up.\n * Wins over locale defaults. Pass partial overrides — `decimal`\n * alone is valid; the unspecified separator stays at the locale\n * default.\n */\n animateSeparators?: {\n thousand?: string;\n decimal?: string;\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* Stat */\n/* ------------------------------------------------------------------ */\n\nexport const Stat = forwardRef<HTMLDivElement, StatProps>(\n (\n {\n label,\n value,\n format,\n currency,\n locale,\n trend,\n delta,\n deltaFormat = 'decimal',\n icon,\n loading = false,\n animate = false,\n animateOnEveryView = false,\n animateDurationMs = 1600,\n animateLocale,\n animateSeparators,\n variant = 'default',\n size = 'md',\n align = 'start',\n className,\n ...props\n },\n ref,\n ) => {\n const { i18n } = useTranslation();\n const labelId = useId();\n const lang = locale ?? i18n.language;\n\n /* -------------------------------------------------- */\n /* Static formatting — the value as it would appear */\n /* in the non-animated (or post-animation) state. */\n /* -------------------------------------------------- */\n\n const formatValue = useCallback((): string => {\n if (typeof value === 'string') return value;\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat(lang, {\n style: 'currency',\n currency: currency ?? 'USD',\n }).format(value);\n case 'percent':\n return new Intl.NumberFormat(lang, { style: 'percent' }).format(\n value,\n );\n case 'decimal':\n return new Intl.NumberFormat(lang, { style: 'decimal' }).format(\n value,\n );\n default:\n return new Intl.NumberFormat(lang).format(value);\n }\n }, [value, format, lang, currency]);\n\n const staticValue = formatValue();\n\n /* -------------------------------------------------- */\n /* Parse the value into a numeric target for the */\n /* count-up. Numeric values use `value` directly; */\n /* string values are stripped of sign / shorthand / */\n /* locale separators and re-decorated each frame. */\n /* -------------------------------------------------- */\n\n const animationLocale = animateLocale ?? lang;\n const hasWarnedRef = useRef<boolean>(false);\n\n const parsed = useMemo<ParsedValue | null>(() => {\n if (!animate) return null;\n if (typeof value === 'number') {\n // Numeric: target is the number itself; no sign / suffix to\n // re-attach.\n return Number.isFinite(value)\n ? { target: value, sign: '', suffix: '' }\n : null;\n }\n return parseStatValue(value, animationLocale);\n }, [animate, value, animationLocale]);\n\n // Dev-mode warning when an animate-flagged Stat has an unparseable\n // string value. Render static, no throw — one warn per component.\n if (\n animate &&\n typeof value === 'string' &&\n parsed === null &&\n import.meta.env.DEV &&\n !hasWarnedRef.current\n ) {\n hasWarnedRef.current = true;\n console.warn(\n `Stat: value=\"${value}\" couldn't be parsed for the count-up animation. Rendering static.`,\n );\n }\n\n const shouldAnimate = animate && parsed !== null;\n\n /* -------------------------------------------------- */\n /* Wire the hook unconditionally — rules-of-hooks. */\n /* When `shouldAnimate` is false the hook still runs */\n /* but `to === from` so it's a no-op visually. */\n /* -------------------------------------------------- */\n\n const target = parsed?.target ?? 0;\n const sign = parsed?.sign ?? '';\n const suffix = parsed?.suffix ?? '';\n\n // Wrap the count-up formatter so the captured sign + suffix are\n // re-applied every frame. The hook hands us the locale-formatted\n // magnitude; we own the decoration around it.\n const formatter = useCallback(\n (n: number) => {\n // Use the absolute value for the magnitude so the captured\n // sign is the sole source of the rendered sign. Negative\n // numbers passed in (e.g. `to: -12`) come through unsigned;\n // we re-apply via `sign`. When the caller asked for explicit\n // separators, force `useGrouping: true` so the locale's default\n // `min2`/auto behaviour doesn't suppress the thousand glyph.\n const magnitudeNF = (() => {\n try {\n return new Intl.NumberFormat(\n animationLocale,\n animateSeparators ? { useGrouping: true } : undefined,\n );\n } catch {\n return null;\n }\n })();\n\n let magnitudeStr: string;\n if (magnitudeNF) {\n if (animateSeparators) {\n // Hand-build the grouped magnitude so we don't depend on\n // ICU's locale-by-locale grouping defaults (which differ\n // between engines and can drop the group at 4-digit\n // thresholds even with `useGrouping: true`). Locale only\n // influences sign handling, which we apply separately.\n const abs = Math.abs(n);\n const intPart = Math.trunc(abs);\n const fracPart = abs - intPart;\n const groupGlyph = animateSeparators.thousand ?? '';\n const intStr = String(intPart).replace(\n /\\B(?=(\\d{3})+(?!\\d))/g,\n groupGlyph,\n );\n if (fracPart === 0) {\n magnitudeStr = intStr;\n } else {\n const fracStr = fracPart.toFixed(6).replace(/0+$/, '').slice(2);\n const decimalGlyph = animateSeparators.decimal ?? '.';\n magnitudeStr = fracStr ? intStr + decimalGlyph + fracStr : intStr;\n }\n } else {\n magnitudeStr = magnitudeNF.format(Math.abs(n));\n }\n } else {\n magnitudeStr = String(Math.abs(n));\n }\n\n // Re-derive the sign every frame: the captured sign wins, but\n // a negative `to` with no leading sign in the source still\n // renders with a leading \"-\" so the final formatted output\n // matches locale conventions.\n let resolvedSign: string = sign;\n if (sign === '' && n < 0) resolvedSign = '-';\n\n return `${resolvedSign}${magnitudeStr}${suffix}`;\n },\n [animationLocale, animateSeparators, sign, suffix],\n );\n\n const countUp = useCountUp({\n to: target,\n from: 0,\n durationMs: animateDurationMs,\n startOnVisible: true,\n retriggerOnReEntry: animateOnEveryView,\n // `formatter` is the only path we use — it owns both the locale\n // formatting and the sign/suffix re-application.\n formatter,\n });\n\n /* -------------------------------------------------- */\n /* Loading state */\n /* -------------------------------------------------- */\n\n if (loading) {\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-busy=\"true\"\n aria-label={label}\n data-component=\"stat\"\n className={statVariants({ variant, size, align, className })}\n {...props}\n >\n <div className=\"ds:h-[var(--skeleton-label-h)] ds:w-[var(--skeleton-label-w)] ds:animate-pulse ds:rounded-[var(--radius-sm)] ds:bg-[var(--muted)]\" />\n <div className=\"ds:h-[var(--skeleton-value-h)] ds:w-[var(--skeleton-value-w)] ds:animate-pulse ds:rounded-[var(--radius-sm)] ds:bg-[var(--muted)]\" />\n </div>\n );\n }\n\n /* -------------------------------------------------- */\n /* Render */\n /* -------------------------------------------------- */\n\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-labelledby={labelId}\n data-component=\"stat\"\n className={statVariants({ variant, size, align, className })}\n {...props}\n >\n <span className=\"ds:inline-flex ds:items-center\">\n {icon && (\n <span\n aria-hidden=\"true\"\n className={[\n 'ds:me-[var(--spacing-xs)] ds:inline-flex ds:shrink-0',\n // Accent tint on the leading icon. The icon is decorative\n // (aria-hidden), so magenta-500's 3.2:1 contrast on white\n // is acceptable here — the Stat label + value still\n // carry the accessible name at `--foreground`.\n 'ds:text-[color:var(--accent)]',\n 'ds:forced-colors:text-[CanvasText]',\n ].join(' ')}\n >\n {icon}\n </span>\n )}\n <span\n id={labelId}\n className=\"type-label ds:text-[var(--muted-foreground)]\"\n >\n {label}\n </span>\n </span>\n {shouldAnimate ? (\n // The animated number is wrapped in an `aria-live=\"off\"` span\n // so screen readers don't announce every interpolated frame.\n // The `role=\"group\"` ancestor (with `aria-labelledby` pointing\n // at the label) carries the final accessible reading — the\n // animated frames are decorative only.\n <span\n ref={countUp.ref}\n aria-live=\"off\"\n className={valueVariants({ size, variant })}\n >\n {countUp.value}\n </span>\n ) : (\n <span className={valueVariants({ size, variant })}>\n {staticValue}\n </span>\n )}\n {trend != null && delta != null && (\n <StatTrend\n trend={trend}\n delta={delta}\n deltaFormat={deltaFormat}\n locale={locale}\n />\n )}\n </div>\n );\n },\n);\n\nStat.displayName = 'Stat';\n"],"names":["statVariants","cva","valueVariants","trendVariants","ARROW","TREND_KEY","StatTrend","trend","delta","deltaFormat","locale","t","i18n","useTranslation","formattedDelta","jsx","parseStatValue","raw","trimmed","sign","body","suffix","suffixMatch","group","decimal","probe","part","usStyleMatch","direct","normalised","numeric","Stat","forwardRef","label","value","format","currency","icon","loading","animate","animateOnEveryView","animateDurationMs","animateLocale","animateSeparators","variant","size","align","className","props","ref","labelId","useId","lang","staticValue","useCallback","animationLocale","useRef","parsed","useMemo","shouldAnimate","target","formatter","n","magnitudeNF","magnitudeStr","abs","intPart","fracPart","groupGlyph","intStr","fracStr","decimalGlyph","resolvedSign","countUp","useCountUp","jsxs"],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useMemo as D, useState as T, useRef as e, useCallback as v, useEffect as U } from "react";
|
|
2
|
+
const H = (l) => 1 - Math.pow(1 - l, 3);
|
|
3
|
+
function J(l, u, d) {
|
|
4
|
+
if (d) return d;
|
|
5
|
+
let m;
|
|
6
|
+
try {
|
|
7
|
+
m = new Intl.NumberFormat(l);
|
|
8
|
+
} catch {
|
|
9
|
+
return (r) => String(r);
|
|
10
|
+
}
|
|
11
|
+
return u ? (r) => {
|
|
12
|
+
if (!Number.isFinite(r)) return String(r);
|
|
13
|
+
const o = r < 0 ? "-" : "", w = Math.abs(r), p = Math.trunc(w), g = w - p, R = String(p), P = u.thousand ?? "", i = R.replace(/\B(?=(\d{3})+(?!\d))/g, P);
|
|
14
|
+
if (g === 0) return o + i;
|
|
15
|
+
const F = g.toFixed(6).replace(/0+$/, "").slice(2), t = u.decimal ?? ".";
|
|
16
|
+
return F ? o + i + t + F : o + i;
|
|
17
|
+
} : (r) => m.format(r);
|
|
18
|
+
}
|
|
19
|
+
function K() {
|
|
20
|
+
return typeof window > "u" || typeof window.matchMedia != "function" ? !1 : window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
21
|
+
}
|
|
22
|
+
function W(l) {
|
|
23
|
+
const {
|
|
24
|
+
to: u,
|
|
25
|
+
from: d = 0,
|
|
26
|
+
durationMs: m = 1600,
|
|
27
|
+
startOnVisible: r = !0,
|
|
28
|
+
retriggerOnReEntry: o = !1,
|
|
29
|
+
locale: w,
|
|
30
|
+
separators: p,
|
|
31
|
+
formatter: g,
|
|
32
|
+
reduceMotion: R
|
|
33
|
+
} = l, P = D(
|
|
34
|
+
() => J(
|
|
35
|
+
w ?? (typeof navigator < "u" ? navigator.language : void 0),
|
|
36
|
+
p,
|
|
37
|
+
g
|
|
38
|
+
),
|
|
39
|
+
[w, p, g]
|
|
40
|
+
), i = D(() => typeof R == "boolean" ? R : K(), [R]), [F, t] = T(u), [$, b] = T(!0), E = e(null), n = e(null), s = e(null), f = e(null), h = e(!1), O = e(d), S = e(u), G = e(m), x = e(i), V = e(o), A = e(r);
|
|
41
|
+
O.current = d, S.current = u, G.current = m, x.current = i, V.current = o, A.current = r;
|
|
42
|
+
const c = v(() => {
|
|
43
|
+
s.current != null && (typeof window < "u" && window.cancelAnimationFrame(s.current), s.current = null), f.current = null;
|
|
44
|
+
}, []), k = v((a) => {
|
|
45
|
+
f.current == null && (f.current = a);
|
|
46
|
+
const C = a - f.current, I = Math.max(G.current, 0), M = I === 0 ? 1 : Math.min(C / I, 1), j = H(M), N = O.current, B = S.current, z = N + (B - N) * j;
|
|
47
|
+
if (M >= 1) {
|
|
48
|
+
t(B), b(!0), s.current = null, f.current = null;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
t(z), s.current = window.requestAnimationFrame(k);
|
|
52
|
+
}, []), y = v(() => {
|
|
53
|
+
if (!(typeof window > "u")) {
|
|
54
|
+
if (x.current) {
|
|
55
|
+
c(), t(S.current), b(!0), h.current = !0;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
c(), t(O.current), b(!1), f.current = null, s.current = window.requestAnimationFrame(k), h.current = !0;
|
|
59
|
+
}
|
|
60
|
+
}, [c, k]), q = v(() => {
|
|
61
|
+
c(), t(O.current), b(!1), h.current = !1;
|
|
62
|
+
}, [c]);
|
|
63
|
+
return U(() => {
|
|
64
|
+
if (!(typeof window > "u")) {
|
|
65
|
+
if (x.current) {
|
|
66
|
+
t(S.current), b(!0);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
A.current || y();
|
|
70
|
+
}
|
|
71
|
+
}, []), U(() => () => {
|
|
72
|
+
c(), n.current && (n.current.disconnect(), n.current = null);
|
|
73
|
+
}, [c]), {
|
|
74
|
+
ref: v(
|
|
75
|
+
(a) => {
|
|
76
|
+
if (a === null) {
|
|
77
|
+
E.current = null, n.current && (n.current.disconnect(), n.current = null);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (E.current = a, typeof window > "u" || x.current || !A.current || n.current) return;
|
|
81
|
+
if (typeof IntersectionObserver > "u") {
|
|
82
|
+
y();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const C = new IntersectionObserver(
|
|
86
|
+
(I) => {
|
|
87
|
+
const M = I[0];
|
|
88
|
+
if (M)
|
|
89
|
+
if (M.isIntersecting) {
|
|
90
|
+
if (h.current && !V.current) return;
|
|
91
|
+
y();
|
|
92
|
+
} else V.current && h.current && q();
|
|
93
|
+
},
|
|
94
|
+
{ threshold: 0.4 }
|
|
95
|
+
);
|
|
96
|
+
C.observe(a), n.current = C;
|
|
97
|
+
},
|
|
98
|
+
[y, q]
|
|
99
|
+
),
|
|
100
|
+
value: P(F),
|
|
101
|
+
done: $,
|
|
102
|
+
start: y,
|
|
103
|
+
reset: q
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
W as u
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=use-count-up-BLLetaZ8.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-count-up-BLLetaZ8.js","sources":["../../src/hooks/use-count-up.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\n/* -------------------------------------------------------------------- */\n/* Types */\n/* -------------------------------------------------------------------- */\n\nexport interface UseCountUpOptions {\n /** Target value the animation lands on. */\n to: number;\n /** Starting value. Defaults to 0. */\n from?: number;\n /** Duration in milliseconds. Defaults to 1600. */\n durationMs?: number;\n /**\n * Attach an `IntersectionObserver` to the ref and only start the\n * animation when the element first crosses the visibility threshold.\n * Defaults to `true`. Set to `false` to start immediately on mount.\n */\n startOnVisible?: boolean;\n /**\n * When `true`, every out-of-view → in-view transition resets to\n * `from` and re-plays the animation. Defaults to `false` — the\n * default is \"animate once per element per session\", matching the\n * booking-website hero use-case.\n */\n retriggerOnReEntry?: boolean;\n /** BCP 47 locale. Falls back to `navigator.language` then `'en'`. */\n locale?: string;\n /**\n * Override the locale's default thousand / decimal glyphs. When set,\n * the hook builds a custom formatter on top of `Intl.NumberFormat`\n * via `formatToParts`. Pass partial overrides — `decimal` alone is\n * valid; the unspecified separator stays at the locale default.\n */\n separators?: {\n thousand?: string;\n decimal?: string;\n };\n /**\n * Full override — when provided, the hook bypasses\n * `Intl.NumberFormat` and runs every frame's numeric value through\n * this function instead. Highest precedence; `locale` and\n * `separators` are ignored for the duration of the animation. The\n * sign is part of the number passed in.\n */\n formatter?: (n: number) => string;\n /**\n * Forces the reduced-motion branch on. Defaults to reading\n * `prefers-reduced-motion: reduce` via `window.matchMedia`. Useful\n * for unit tests and for callers that already track motion\n * preferences upstream.\n */\n reduceMotion?: boolean;\n}\n\nexport interface UseCountUpReturn {\n /**\n * Ref callback. Attach to the element that should trigger the\n * animation on viewport entry. Passing `null` (unmounting) cleans\n * up the IntersectionObserver.\n */\n ref: (el: HTMLElement | null) => void;\n /** Formatted current frame value. */\n value: string;\n /** `true` after the animation lands on `to`. */\n done: boolean;\n /**\n * Manually start the animation. Useful when `startOnVisible` is\n * `false` or when re-playing is gated on a non-viewport event.\n */\n start: () => void;\n /** Reset to `from`; the next `start` (or visibility entry) re-plays. */\n reset: () => void;\n}\n\n/* -------------------------------------------------------------------- */\n/* Easing */\n/* -------------------------------------------------------------------- */\n\n// `easeOutCubic` — fast at first, slowly settles to the target. Matches\n// the kit's motion guidelines for \"decisive entrance\" animations.\nconst easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3);\n\n/* -------------------------------------------------------------------- */\n/* Formatter factory */\n/* -------------------------------------------------------------------- */\n\ntype Separators = NonNullable<UseCountUpOptions['separators']>;\n\n/**\n * Build the per-frame number formatter. Three branches:\n *\n * 1. Caller passed `formatter` → use it verbatim.\n * 2. Caller passed `separators` → use `Intl.NumberFormat.formatToParts`\n * and substitute the locale's `group` / `decimal` parts with the\n * override glyphs. This preserves digit shaping (Arabic-Indic,\n * Devanagari) while swapping only the separator characters.\n * 3. Neither → use plain `Intl.NumberFormat(locale)`.\n *\n * SSR fallback: if `Intl.NumberFormat` is unavailable for the requested\n * locale (it always is in Node 22+, but the closure tolerates a `throw`\n * regardless) we fall back to `String(n)` so the final value still\n * renders.\n */\nfunction buildFormatter(\n locale: string | undefined,\n separators: Separators | undefined,\n formatter: ((n: number) => string) | undefined,\n): (n: number) => string {\n if (formatter) return formatter;\n\n let nf: Intl.NumberFormat;\n try {\n nf = new Intl.NumberFormat(locale);\n } catch {\n return (n: number) => String(n);\n }\n\n if (!separators) return (n: number) => nf.format(n);\n\n // When the caller passes explicit separators we build the integer +\n // fraction directly so the result doesn't depend on ICU's locale-by-\n // locale grouping defaults (some engines suppress the group at\n // 4-digit thresholds via `useGrouping: 'min2'`). The locale only\n // influences the sign formatting, which we delegate to a plain\n // `Intl.NumberFormat` over the absolute integer part.\n return (n: number) => {\n if (!Number.isFinite(n)) return String(n);\n const sign = n < 0 ? '-' : '';\n const abs = Math.abs(n);\n const intPart = Math.trunc(abs);\n const fracPart = abs - intPart;\n const intStr = String(intPart);\n // Insert the thousand glyph every three digits from the right.\n const groupGlyph = separators.thousand ?? '';\n const grouped = intStr.replace(/\\B(?=(\\d{3})+(?!\\d))/g, groupGlyph);\n if (fracPart === 0) return sign + grouped;\n // Up to 6 decimal places, trimmed — keeps `1500.25` rendering as\n // `1 500,25` not `1 500,250000`. ICU's default fraction-digit logic\n // matches `toString()` for most marketing-stat use cases.\n const fracStr = fracPart.toFixed(6).replace(/0+$/, '').slice(2);\n const decimalGlyph = separators.decimal ?? '.';\n return fracStr ? sign + grouped + decimalGlyph + fracStr : sign + grouped;\n };\n}\n\n/* -------------------------------------------------------------------- */\n/* Reduced-motion read */\n/* -------------------------------------------------------------------- */\n\nfunction readPrefersReducedMotion(): boolean {\n if (typeof window === 'undefined') return false;\n if (typeof window.matchMedia !== 'function') return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\n/* -------------------------------------------------------------------- */\n/* Hook */\n/* -------------------------------------------------------------------- */\n\n/**\n * Animate a number from `from` to `to` over `durationMs`, optionally\n * gated on viewport entry. Uses raw `requestAnimationFrame` + an\n * `IntersectionObserver`, no third-party dependency.\n *\n * SSR-safe — the first render (server + client first-paint) returns the\n * final formatted value. The IntersectionObserver and the rAF loop are\n * attached inside `useEffect` so SSR markup matches client first-paint\n * and hydration never observes the `from` value.\n *\n * Reduced motion — when `prefers-reduced-motion: reduce` is active (or\n * `reduceMotion: true` is passed) the hook short-circuits to the final\n * value on first paint and never attaches the observer. Honoured even\n * when `retriggerOnReEntry` is set.\n *\n * Direction & negatives — the hook animates from `from` to `to`\n * whichever direction that is. `to` can be negative; the sign is part\n * of the formatted output every frame.\n */\nexport function useCountUp(options: UseCountUpOptions): UseCountUpReturn {\n const {\n to,\n from = 0,\n durationMs = 1600,\n startOnVisible = true,\n retriggerOnReEntry = false,\n locale,\n separators,\n formatter,\n reduceMotion,\n } = options;\n\n // Build the formatter once per render — it's cheap, and folding it\n // into a useMemo keyed on its inputs lets locale/separators changes\n // re-render the current frame value through the new formatter.\n const format = useMemo(\n () =>\n buildFormatter(\n locale ??\n (typeof navigator !== 'undefined' ? navigator.language : undefined),\n separators,\n formatter,\n ),\n [locale, separators, formatter],\n );\n\n // Resolve reduced-motion preference. Caller wins over the media query\n // read; the media query read is itself null-safe for SSR.\n const isReducedMotion = useMemo(() => {\n if (typeof reduceMotion === 'boolean') return reduceMotion;\n return readPrefersReducedMotion();\n }, [reduceMotion]);\n\n // SSR + first-paint render — emit the final value. The first useEffect\n // pass will swap to `from` and kick off the animation if not reduced.\n const [current, setCurrent] = useState<number>(to);\n const [done, setDone] = useState<boolean>(true);\n\n // Track the element + the observer + the active rAF handle. Refs\n // because none of these need to drive re-renders.\n const elementRef = useRef<HTMLElement | null>(null);\n const observerRef = useRef<IntersectionObserver | null>(null);\n const rafRef = useRef<number | null>(null);\n const startTimestampRef = useRef<number | null>(null);\n const hasPlayedRef = useRef<boolean>(false);\n // Mirrors of the latest option values so callbacks created once at\n // mount can still read fresh values without ballooning the dep array.\n const fromRef = useRef<number>(from);\n const toRef = useRef<number>(to);\n const durationRef = useRef<number>(durationMs);\n const reducedRef = useRef<boolean>(isReducedMotion);\n const retriggerRef = useRef<boolean>(retriggerOnReEntry);\n const startOnVisibleRef = useRef<boolean>(startOnVisible);\n\n // Sync mirrors on every render — cheap and keeps the imperative\n // start/reset callbacks reading current values.\n fromRef.current = from;\n toRef.current = to;\n durationRef.current = durationMs;\n reducedRef.current = isReducedMotion;\n retriggerRef.current = retriggerOnReEntry;\n startOnVisibleRef.current = startOnVisible;\n\n /* ------------------------------------------------------------------ */\n /* Animation loop */\n /* ------------------------------------------------------------------ */\n\n const cancelRaf = useCallback(() => {\n if (rafRef.current != null) {\n if (typeof window !== 'undefined') {\n window.cancelAnimationFrame(rafRef.current);\n }\n rafRef.current = null;\n }\n startTimestampRef.current = null;\n }, []);\n\n const tick = useCallback((timestamp: number) => {\n if (startTimestampRef.current == null) {\n startTimestampRef.current = timestamp;\n }\n const elapsed = timestamp - startTimestampRef.current;\n const duration = Math.max(durationRef.current, 0);\n const t = duration === 0 ? 1 : Math.min(elapsed / duration, 1);\n const eased = easeOutCubic(t);\n const fromV = fromRef.current;\n const toV = toRef.current;\n const next = fromV + (toV - fromV) * eased;\n\n if (t >= 1) {\n // Land exactly on `to` to avoid rounding noise in the final\n // formatted frame.\n setCurrent(toV);\n setDone(true);\n rafRef.current = null;\n startTimestampRef.current = null;\n return;\n }\n\n setCurrent(next);\n rafRef.current = window.requestAnimationFrame(tick);\n }, []);\n\n const start = useCallback(() => {\n if (typeof window === 'undefined') return;\n\n if (reducedRef.current) {\n // Reduced-motion short-circuit — paint the final value, no rAF.\n cancelRaf();\n setCurrent(toRef.current);\n setDone(true);\n hasPlayedRef.current = true;\n return;\n }\n\n cancelRaf();\n setCurrent(fromRef.current);\n setDone(false);\n startTimestampRef.current = null;\n rafRef.current = window.requestAnimationFrame(tick);\n hasPlayedRef.current = true;\n }, [cancelRaf, tick]);\n\n const reset = useCallback(() => {\n cancelRaf();\n setCurrent(fromRef.current);\n setDone(false);\n hasPlayedRef.current = false;\n }, [cancelRaf]);\n\n /* ------------------------------------------------------------------ */\n /* Mount — kick off / observe */\n /* ------------------------------------------------------------------ */\n\n // First effect pass runs only on the client. SSR + hydration both\n // produced the final value; we now decide whether to animate.\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n if (reducedRef.current) {\n // Make sure we're parked on the final value and no observer is\n // attached. Honoured even when `retriggerOnReEntry` is true.\n setCurrent(toRef.current);\n setDone(true);\n return;\n }\n\n if (!startOnVisibleRef.current) {\n // Auto-start on mount when the caller opts out of visibility\n // gating.\n start();\n }\n // We intentionally run this only on the first mount; subsequent\n // updates to options are mirrored through the refs above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Cleanup on unmount.\n useEffect(() => {\n return () => {\n cancelRaf();\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }, [cancelRaf]);\n\n /* ------------------------------------------------------------------ */\n /* Ref callback */\n /* ------------------------------------------------------------------ */\n\n const ref = useCallback(\n (el: HTMLElement | null) => {\n // Detaching — disconnect and clear refs.\n if (el === null) {\n elementRef.current = null;\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n return;\n }\n\n elementRef.current = el;\n\n // SSR / non-DOM env — nothing else to do.\n if (typeof window === 'undefined') return;\n\n // Reduced motion — no observer, value is already final.\n if (reducedRef.current) return;\n\n // Caller opted out of visibility gating — `start` already fired\n // from the mount effect above.\n if (!startOnVisibleRef.current) return;\n\n // Already observing — no-op. (Strict-mode double-invoke calls\n // the ref callback with the same node twice in dev.)\n if (observerRef.current) return;\n\n if (typeof IntersectionObserver === 'undefined') {\n // No IO available (e.g. some headless test envs without a\n // shim) — start immediately and skip the observer.\n start();\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n const entry = entries[0];\n if (!entry) return;\n\n if (entry.isIntersecting) {\n // Default behaviour: animate once per element per session.\n // Re-entry only re-plays when the caller opted in.\n if (hasPlayedRef.current && !retriggerRef.current) return;\n start();\n } else if (retriggerRef.current && hasPlayedRef.current) {\n // Out-of-view while in retrigger mode — reset so the next\n // entry re-animates from `from`.\n reset();\n }\n },\n { threshold: 0.4 },\n );\n\n observer.observe(el);\n observerRef.current = observer;\n },\n [start, reset],\n );\n\n return {\n ref,\n value: format(current),\n done,\n start,\n reset,\n };\n}\n"],"names":["easeOutCubic","t","buildFormatter","locale","separators","formatter","nf","n","sign","abs","intPart","fracPart","intStr","groupGlyph","grouped","fracStr","decimalGlyph","readPrefersReducedMotion","useCountUp","options","to","from","durationMs","startOnVisible","retriggerOnReEntry","reduceMotion","format","useMemo","isReducedMotion","current","setCurrent","useState","done","setDone","elementRef","useRef","observerRef","rafRef","startTimestampRef","hasPlayedRef","fromRef","toRef","durationRef","reducedRef","retriggerRef","startOnVisibleRef","cancelRaf","useCallback","tick","timestamp","elapsed","duration","eased","fromV","toV","next","start","reset","useEffect","el","observer","entries","entry"],"mappings":"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"}
|
package/dist/agent-catalog.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"packageVersion": "0.
|
|
3
|
+
"packageVersion": "0.15.0",
|
|
4
4
|
"components": [
|
|
5
5
|
{
|
|
6
6
|
"kind": "component",
|
|
@@ -544,6 +544,59 @@
|
|
|
544
544
|
}
|
|
545
545
|
}
|
|
546
546
|
},
|
|
547
|
+
{
|
|
548
|
+
"kind": "component",
|
|
549
|
+
"id": "carousel",
|
|
550
|
+
"capabilities": [
|
|
551
|
+
"navigate",
|
|
552
|
+
"view_change"
|
|
553
|
+
],
|
|
554
|
+
"state": [
|
|
555
|
+
{
|
|
556
|
+
"name": "activeIndex",
|
|
557
|
+
"type": "number",
|
|
558
|
+
"description": "Zero-based index of the currently snapped slide."
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"name": "count",
|
|
562
|
+
"type": "number",
|
|
563
|
+
"description": "Total slide count."
|
|
564
|
+
}
|
|
565
|
+
],
|
|
566
|
+
"actions": [
|
|
567
|
+
{
|
|
568
|
+
"name": "scroll_to",
|
|
569
|
+
"safety": "read",
|
|
570
|
+
"argsType": "{ index: number }",
|
|
571
|
+
"description": "Scroll to the slide at the given index."
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"name": "scroll_prev",
|
|
575
|
+
"safety": "read",
|
|
576
|
+
"description": "Scroll to the previous slide."
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"name": "scroll_next",
|
|
580
|
+
"safety": "read",
|
|
581
|
+
"description": "Scroll to the next slide."
|
|
582
|
+
}
|
|
583
|
+
],
|
|
584
|
+
"domHooks": {
|
|
585
|
+
"root": {
|
|
586
|
+
"attr": "data-component",
|
|
587
|
+
"value": "carousel"
|
|
588
|
+
},
|
|
589
|
+
"instanceId": {
|
|
590
|
+
"attr": "data-component-id",
|
|
591
|
+
"sourceProp": "id",
|
|
592
|
+
"description": "Sourced from the id prop on Carousel."
|
|
593
|
+
},
|
|
594
|
+
"item": {
|
|
595
|
+
"attr": "data-slide-id",
|
|
596
|
+
"description": "Each Carousel.Item emits its `id` prop as `data-slide-id`. `data-slide-index` carries the zero-based position alongside."
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
},
|
|
547
600
|
{
|
|
548
601
|
"kind": "component",
|
|
549
602
|
"id": "chart",
|
|
@@ -2431,6 +2484,44 @@
|
|
|
2431
2484
|
}
|
|
2432
2485
|
}
|
|
2433
2486
|
},
|
|
2487
|
+
{
|
|
2488
|
+
"kind": "component",
|
|
2489
|
+
"id": "public-header",
|
|
2490
|
+
"capabilities": [
|
|
2491
|
+
"open",
|
|
2492
|
+
"close"
|
|
2493
|
+
],
|
|
2494
|
+
"state": [
|
|
2495
|
+
{
|
|
2496
|
+
"name": "menuOpen",
|
|
2497
|
+
"type": "boolean",
|
|
2498
|
+
"description": "Whether the mobile drawer is currently open."
|
|
2499
|
+
}
|
|
2500
|
+
],
|
|
2501
|
+
"actions": [
|
|
2502
|
+
{
|
|
2503
|
+
"name": "open_menu",
|
|
2504
|
+
"safety": "read",
|
|
2505
|
+
"description": "Open the mobile drawer."
|
|
2506
|
+
},
|
|
2507
|
+
{
|
|
2508
|
+
"name": "close_menu",
|
|
2509
|
+
"safety": "read",
|
|
2510
|
+
"description": "Close the mobile drawer."
|
|
2511
|
+
}
|
|
2512
|
+
],
|
|
2513
|
+
"domHooks": {
|
|
2514
|
+
"root": {
|
|
2515
|
+
"attr": "data-component",
|
|
2516
|
+
"value": "public-header"
|
|
2517
|
+
},
|
|
2518
|
+
"instanceId": {
|
|
2519
|
+
"attr": "data-component-id",
|
|
2520
|
+
"sourceProp": "id",
|
|
2521
|
+
"description": "Sourced from the id prop on PublicHeader."
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
},
|
|
2434
2525
|
{
|
|
2435
2526
|
"kind": "component",
|
|
2436
2527
|
"id": "radio",
|