@astra-kit/ui 1.0.0 → 1.2.1

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 (43) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +14 -3
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/components/button-link.d.ts +15 -0
  5. package/dist/components/button-link.js +70 -0
  6. package/dist/components/button-recipe.d.ts +27 -0
  7. package/dist/components/button-recipe.js +44 -0
  8. package/dist/components/button.d.ts +3 -5
  9. package/dist/components/button.js +1 -25
  10. package/dist/components/empty-state.d.ts +20 -0
  11. package/dist/components/empty-state.js +357 -0
  12. package/dist/components/metric-card.d.ts +22 -0
  13. package/dist/components/metric-card.js +378 -0
  14. package/dist/components/radio-group.d.ts +5 -3
  15. package/dist/components/radio-group.js +88 -85
  16. package/dist/components/stepper.d.ts +39 -0
  17. package/dist/components/stepper.js +693 -0
  18. package/dist/components/timeline.d.ts +51 -0
  19. package/dist/components/timeline.js +1013 -0
  20. package/dist/components/usage-meter.d.ts +39 -0
  21. package/dist/components/usage-meter.js +1093 -0
  22. package/dist/constants/empty-state.d.ts +12 -0
  23. package/dist/constants/empty-state.js +12 -0
  24. package/dist/constants/metric-card.d.ts +12 -0
  25. package/dist/constants/metric-card.js +12 -0
  26. package/dist/constants/stepper.d.ts +52 -0
  27. package/dist/constants/stepper.js +23 -0
  28. package/dist/constants/timeline.d.ts +62 -0
  29. package/dist/constants/timeline.js +31 -0
  30. package/dist/constants/usage-meter.d.ts +100 -0
  31. package/dist/constants/usage-meter.js +38 -0
  32. package/dist/hooks/use-has-entered-view.d.ts +9 -0
  33. package/dist/hooks/use-has-entered-view.js +46 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.js +8 -0
  36. package/dist/motion/arc-odometer.d.ts +53 -0
  37. package/dist/motion/arc-odometer.js +424 -0
  38. package/dist/motion/arc-text-swap.d.ts +22 -0
  39. package/dist/motion/arc-text-swap.js +142 -0
  40. package/dist/motion/arc-tokens.d.ts +1 -0
  41. package/dist/motion/arc-tokens.js +2 -1
  42. package/dist/styles.css +1 -1
  43. package/package.json +31 -1
@@ -0,0 +1,357 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ /**
4
+ * Arc's Empty state (https://uiarc.dev, MIT License, see THIRD_PARTY_NOTICES.md): a useful next step when there is
5
+ * nothing to show yet. A centered icon, title, description and optional action. When the copy changes it rolls in place
6
+ * while the block height springs to fit, and a different icon component crossfades in.
7
+ */
8
+ import { isValidElement, useEffect, useLayoutEffect, useRef } from "react";
9
+ import { AnimatePresence, animate, motion, useMotionValue } from "motion/react";
10
+ import { Folder } from "lucide-react";
11
+ import { EMPTY_STATE_HEIGHT_MORPH_WINDOW_MS, EMPTY_STATE_ICON_ENTER_SCALE, EMPTY_STATE_ICON_SIZE_PX, EMPTY_STATE_ICON_STROKE_WIDTH } from "../constants/empty-state.js";
12
+ import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
13
+ import { cn } from "../lib/utils.js";
14
+ import { ArcTextSwap } from "../motion/arc-text-swap.js";
15
+ import { ARC_BLUR, ARC_DURATION, ARC_EASE, ARC_SPRING_SMOOTH, ARC_SPRING_SNAPPY } from "../motion/arc-tokens.js";
16
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ const iconEnter = {
18
+ opacity: 0,
19
+ scale: EMPTY_STATE_ICON_ENTER_SCALE,
20
+ filter: `blur(${ARC_BLUR.subtle}px)`
21
+ };
22
+ const iconRest = {
23
+ opacity: 1,
24
+ scale: 1,
25
+ filter: "blur(0px)"
26
+ };
27
+ const iconExit = {
28
+ ...iconEnter,
29
+ transition: {
30
+ duration: ARC_DURATION.fast,
31
+ ease: [...ARC_EASE.standard]
32
+ }
33
+ };
34
+ const reducedFade = { opacity: 0 };
35
+ const reducedExit = {
36
+ opacity: 0,
37
+ transition: { duration: ARC_DURATION.instant }
38
+ };
39
+ const reducedTransition = { duration: ARC_DURATION.instant };
40
+ /**
41
+ * Keys an icon by its component, so a new icon component crossfades in while re-rendering the same icon stays still.
42
+ * @param icon - The icon element.
43
+ * @returns A stable key for the icon's component.
44
+ */
45
+ function iconKey(icon) {
46
+ if (!isValidElement(icon)) return "icon";
47
+ const type = icon.type;
48
+ return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
49
+ }
50
+ /**
51
+ * Follows its content height. After `morphKey` changes the height springs from the old size to the new one and then
52
+ * returns to auto, so passive reflows follow instantly. It clips only while moving, so focus outlines stay visible.
53
+ */
54
+ function HeightFrame(t0) {
55
+ const $ = _c(14);
56
+ const { reduceMotion, morphKey, children } = t0;
57
+ const frame = useRef(null);
58
+ const content = useRef(null);
59
+ const height = useMotionValue("auto");
60
+ const changedAt = useRef(0);
61
+ let t1;
62
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
63
+ t1 = () => {
64
+ changedAt.current = performance.now();
65
+ };
66
+ $[0] = t1;
67
+ } else {
68
+ t1 = $[0];
69
+ }
70
+ let t2;
71
+ if ($[1] !== morphKey) {
72
+ t2 = [morphKey];
73
+ $[1] = morphKey;
74
+ $[2] = t2;
75
+ } else {
76
+ t2 = $[2];
77
+ }
78
+ useLayoutEffect(t1, t2);
79
+ let t3;
80
+ let t4;
81
+ if ($[3] !== height || $[4] !== reduceMotion) {
82
+ t3 = () => {
83
+ const node = content.current;
84
+ if (!node || typeof ResizeObserver === "undefined") {
85
+ return;
86
+ }
87
+ let lastHeight;
88
+ let controls;
89
+ const settle = () => {
90
+ height.jump("auto");
91
+ if (frame.current) {
92
+ Object.assign(frame.current.style, {
93
+ overflow: "",
94
+ height: "auto"
95
+ });
96
+ }
97
+ };
98
+ const observer = new ResizeObserver((t5) => {
99
+ const [entry] = t5;
100
+ if (!entry) {
101
+ return;
102
+ }
103
+ const next = entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
104
+ const current = height.get();
105
+ const from = typeof current === "number" ? current : lastHeight;
106
+ lastHeight = next;
107
+ controls?.stop();
108
+ if (reduceMotion || from === undefined || from === next || performance.now() - changedAt.current > EMPTY_STATE_HEIGHT_MORPH_WINDOW_MS) {
109
+ settle();
110
+ return;
111
+ }
112
+ if (frame.current) {
113
+ Object.assign(frame.current.style, {
114
+ overflow: "hidden",
115
+ height: `${from}px`
116
+ });
117
+ }
118
+ controls = animate(height, [from, next], {
119
+ ...ARC_SPRING_SMOOTH,
120
+ onComplete: settle
121
+ });
122
+ });
123
+ observer.observe(node);
124
+ return () => {
125
+ observer.disconnect();
126
+ controls?.stop();
127
+ };
128
+ };
129
+ t4 = [height, reduceMotion];
130
+ $[3] = height;
131
+ $[4] = reduceMotion;
132
+ $[5] = t3;
133
+ $[6] = t4;
134
+ } else {
135
+ t3 = $[5];
136
+ t4 = $[6];
137
+ }
138
+ useEffect(t3, t4);
139
+ let t5;
140
+ if ($[7] !== height) {
141
+ t5 = { height };
142
+ $[7] = height;
143
+ $[8] = t5;
144
+ } else {
145
+ t5 = $[8];
146
+ }
147
+ let t6;
148
+ if ($[9] !== children) {
149
+ t6 = /* @__PURE__ */ _jsx("div", {
150
+ ref: content,
151
+ className: "flow-root",
152
+ children
153
+ });
154
+ $[9] = children;
155
+ $[10] = t6;
156
+ } else {
157
+ t6 = $[10];
158
+ }
159
+ let t7;
160
+ if ($[11] !== t5 || $[12] !== t6) {
161
+ t7 = /* @__PURE__ */ _jsx(motion.div, {
162
+ ref: frame,
163
+ "data-slot": "empty-state-copy",
164
+ className: "w-full max-w-full",
165
+ style: t5,
166
+ children: t6
167
+ });
168
+ $[11] = t5;
169
+ $[12] = t6;
170
+ $[13] = t7;
171
+ } else {
172
+ t7 = $[13];
173
+ }
174
+ return t7;
175
+ }
176
+ /**
177
+ * A centered placeholder for empty views with an icon, a title, a description and an optional action.
178
+ * @param props - Copy, icon, action, accessible name and native section attributes.
179
+ * @returns The empty state section.
180
+ */
181
+ function EmptyState(t0) {
182
+ const $ = _c(40);
183
+ let action;
184
+ let className;
185
+ let description;
186
+ let icon;
187
+ let label;
188
+ let props;
189
+ let title;
190
+ if ($[0] !== t0) {
191
+ ({title, description, action, icon, label, className, ...props} = t0);
192
+ $[0] = t0;
193
+ $[1] = action;
194
+ $[2] = className;
195
+ $[3] = description;
196
+ $[4] = icon;
197
+ $[5] = label;
198
+ $[6] = props;
199
+ $[7] = title;
200
+ } else {
201
+ action = $[1];
202
+ className = $[2];
203
+ description = $[3];
204
+ icon = $[4];
205
+ label = $[5];
206
+ props = $[6];
207
+ title = $[7];
208
+ }
209
+ const reduceMotion = usePrefersReducedMotion();
210
+ let t1;
211
+ if ($[8] !== icon) {
212
+ t1 = icon ?? /* @__PURE__ */ _jsx(Folder, {
213
+ width: EMPTY_STATE_ICON_SIZE_PX,
214
+ height: EMPTY_STATE_ICON_SIZE_PX,
215
+ strokeWidth: EMPTY_STATE_ICON_STROKE_WIDTH
216
+ });
217
+ $[8] = icon;
218
+ $[9] = t1;
219
+ } else {
220
+ t1 = $[9];
221
+ }
222
+ const glyph = t1;
223
+ let t2;
224
+ if ($[10] !== className) {
225
+ t2 = cn("flex w-full flex-col items-center px-(--space-5) py-[clamp(var(--space-8),8vw,var(--space-12))] text-center text-foreground", className);
226
+ $[10] = className;
227
+ $[11] = t2;
228
+ } else {
229
+ t2 = $[11];
230
+ }
231
+ let t3;
232
+ if ($[12] !== glyph) {
233
+ t3 = iconKey(glyph);
234
+ $[12] = glyph;
235
+ $[13] = t3;
236
+ } else {
237
+ t3 = $[13];
238
+ }
239
+ const t4 = reduceMotion ? reducedFade : iconEnter;
240
+ const t5 = reduceMotion ? reducedExit : iconExit;
241
+ const t6 = reduceMotion ? reducedTransition : ARC_SPRING_SNAPPY;
242
+ let t7;
243
+ if ($[14] !== glyph || $[15] !== t3 || $[16] !== t4 || $[17] !== t5 || $[18] !== t6) {
244
+ t7 = /* @__PURE__ */ _jsx("div", {
245
+ "data-slot": "empty-state-icon",
246
+ "aria-hidden": "true",
247
+ className: "relative grid size-(--space-12) place-items-center rounded-(--radius-panel) border border-border bg-surface-muted text-text-secondary animate-[astra-empty-state-icon-enter_var(--duration-considered)_var(--ease-enter)_both] motion-reduce:animate-none",
248
+ children: /* @__PURE__ */ _jsx(AnimatePresence, {
249
+ mode: "popLayout",
250
+ initial: false,
251
+ children: /* @__PURE__ */ _jsx(motion.span, {
252
+ className: "grid place-items-center",
253
+ initial: t4,
254
+ animate: iconRest,
255
+ exit: t5,
256
+ transition: t6,
257
+ children: glyph
258
+ }, t3)
259
+ })
260
+ });
261
+ $[14] = glyph;
262
+ $[15] = t3;
263
+ $[16] = t4;
264
+ $[17] = t5;
265
+ $[18] = t6;
266
+ $[19] = t7;
267
+ } else {
268
+ t7 = $[19];
269
+ }
270
+ const t8 = `${title}\n${description}`;
271
+ let t9;
272
+ if ($[20] !== reduceMotion || $[21] !== title) {
273
+ t9 = /* @__PURE__ */ _jsx("h3", {
274
+ "data-slot": "empty-state-title",
275
+ className: "relative mt-(--space-5) text-base leading-(--leading-body) font-medium text-foreground",
276
+ children: /* @__PURE__ */ _jsx(ArcTextSwap, {
277
+ text: title,
278
+ reduceMotion,
279
+ lineClassName: "text-balance"
280
+ })
281
+ });
282
+ $[20] = reduceMotion;
283
+ $[21] = title;
284
+ $[22] = t9;
285
+ } else {
286
+ t9 = $[22];
287
+ }
288
+ let t10;
289
+ if ($[23] !== description || $[24] !== reduceMotion) {
290
+ t10 = /* @__PURE__ */ _jsx("p", {
291
+ "data-slot": "empty-state-description",
292
+ className: "relative mx-auto mt-(--space-2) max-w-72 text-sm leading-(--leading-body) text-text-secondary",
293
+ children: /* @__PURE__ */ _jsx(ArcTextSwap, {
294
+ text: description,
295
+ reduceMotion,
296
+ lineClassName: "text-balance"
297
+ })
298
+ });
299
+ $[23] = description;
300
+ $[24] = reduceMotion;
301
+ $[25] = t10;
302
+ } else {
303
+ t10 = $[25];
304
+ }
305
+ let t11;
306
+ if ($[26] !== reduceMotion || $[27] !== t10 || $[28] !== t8 || $[29] !== t9) {
307
+ t11 = /* @__PURE__ */ _jsxs(HeightFrame, {
308
+ reduceMotion,
309
+ morphKey: t8,
310
+ children: [t9, t10]
311
+ });
312
+ $[26] = reduceMotion;
313
+ $[27] = t10;
314
+ $[28] = t8;
315
+ $[29] = t9;
316
+ $[30] = t11;
317
+ } else {
318
+ t11 = $[30];
319
+ }
320
+ let t12;
321
+ if ($[31] !== action) {
322
+ t12 = action ? /* @__PURE__ */ _jsx("div", {
323
+ "data-slot": "empty-state-action",
324
+ className: "mt-(--space-5) flex flex-wrap justify-center gap-(--space-3)",
325
+ children: action
326
+ }) : null;
327
+ $[31] = action;
328
+ $[32] = t12;
329
+ } else {
330
+ t12 = $[32];
331
+ }
332
+ let t13;
333
+ if ($[33] !== label || $[34] !== props || $[35] !== t11 || $[36] !== t12 || $[37] !== t2 || $[38] !== t7) {
334
+ t13 = /* @__PURE__ */ _jsxs("section", {
335
+ "data-slot": "empty-state",
336
+ "aria-label": label,
337
+ className: t2,
338
+ ...props,
339
+ children: [
340
+ t7,
341
+ t11,
342
+ t12
343
+ ]
344
+ });
345
+ $[33] = label;
346
+ $[34] = props;
347
+ $[35] = t11;
348
+ $[36] = t12;
349
+ $[37] = t2;
350
+ $[38] = t7;
351
+ $[39] = t13;
352
+ } else {
353
+ t13 = $[39];
354
+ }
355
+ return t13;
356
+ }
357
+ export { EmptyState };
@@ -0,0 +1,22 @@
1
+ import type { ComponentProps } from "react";
2
+ export type MetricCardProps = Omit<ComponentProps<"article">, "children"> & {
3
+ /** What is measured. */
4
+ label: string;
5
+ /** The number. Rolls up from zero on first view, then between values. */
6
+ value: number;
7
+ /** Text after the number, such as "%" or " ms". */
8
+ suffix?: string;
9
+ /** Line under the number, such as "vs. la semana pasada". */
10
+ context: string;
11
+ /** Chip in the top corner, such as "+12%". Write the sign: it sets the colour and is what screen readers hear. */
12
+ change?: string;
13
+ /** Locale of the number's separators. Defaults to `"es-AR"`. */
14
+ locale?: string;
15
+ };
16
+ /**
17
+ * A compact card with a label, an odometer style number, a context line and an optional change chip.
18
+ * @param props - Label, value, suffix, context, change, locale and native article attributes.
19
+ * @returns The metric card.
20
+ */
21
+ declare function MetricCard({ label, value, suffix, context, change, locale, className, ...props }: MetricCardProps): import("react").JSX.Element;
22
+ export { MetricCard };