@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,1013 @@
1
+ "use client";
2
+ import { c as _c } from "react/compiler-runtime";
3
+ /**
4
+ * Arc's Timeline (https://uiarc.dev, MIT License, see THIRD_PARTY_NOTICES.md): follow what happened, newest first,
5
+ * grouped by day. Day labels stay pinned while their updates scroll, the connecting line draws itself as rows come into
6
+ * view, rows with details expand in place, and new updates slide in at the top while the rest glide down. Arrow keys
7
+ * move between expandable rows; Enter or Space expands one.
8
+ */
9
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
10
+ import { AnimatePresence, motion } from "motion/react";
11
+ import { ChevronDown } from "lucide-react";
12
+ import { TIMELINE_AVATAR_SIZE_PX, TIMELINE_DEFAULT_HEADING_LEVEL, TIMELINE_DEFAULT_LOCALE, TIMELINE_DEFAULT_MESSAGES, TIMELINE_DEFAULT_TIME_ZONE, TIMELINE_FRESH_ROW_DELAY_S, TIMELINE_RELATIVE_WINDOW_HOURS, TIMELINE_REVEAL_AMOUNT, TIMELINE_REVEAL_QUEUE_LIMIT_S, TIMELINE_REVEAL_STEP_S } from "../constants/timeline.js";
13
+ import { useIsHydrated } from "../hooks/use-is-hydrated.js";
14
+ import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
15
+ import { cn } from "../lib/utils.js";
16
+ import { ArcTextSwap } from "../motion/arc-text-swap.js";
17
+ import { ARC_BLUR, ARC_DURATION, ARC_EASE, ARC_SPRING_MORPH, ARC_SPRING_SMOOTH, ARC_SPRING_SNAPPY } from "../motion/arc-tokens.js";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ const MILLISECONDS_PER_MINUTE = 6e4;
20
+ const MINUTES_PER_HOUR = 60;
21
+ const MILLISECONDS_PER_HOUR = MILLISECONDS_PER_MINUTE * MINUTES_PER_HOUR;
22
+ /** Length of a `YYYY-MM-DD` day key, the start of an ISO timestamp. */
23
+ const DAY_KEY_LENGTH = 10;
24
+ const MILLISECONDS_PER_SECOND = 1e3;
25
+ const enterEase = [...ARC_EASE.enter];
26
+ const standardEase = [...ARC_EASE.standard];
27
+ /** Quick fades that replace travel with reduced motion. */
28
+ const REDUCED_FADE_S = .15;
29
+ const REDUCED_EXIT_S = .1;
30
+ /** Delays, in seconds, between a row's marker popping and its line drawing, and before new content fades in. */
31
+ const LINE_DRAW_DELAY_S = .1;
32
+ const CONTENT_FADE_DELAY_S = .05;
33
+ const DETAIL_FADE_DELAY_S = .06;
34
+ /** Visual durations, in seconds, of the marker pop, the line draw, a row leaving and details collapsing. */
35
+ const MARKER_POP_DURATION_S = .36;
36
+ const MARKER_POP_BOUNCE = .32;
37
+ const LINE_DRAW_DURATION_S = .34;
38
+ const ROW_EXIT_DURATION_S = .3;
39
+ const DETAIL_COLLAPSE_DURATION_S = .28;
40
+ /** Travel, in pixels, of a new row's content and of expanded details settling in. */
41
+ const FRESH_CONTENT_OFFSET_PX = -10;
42
+ const DETAIL_OFFSET_PX = -4;
43
+ /** Scale a marker pops from. */
44
+ const MARKER_ENTER_SCALE = .4;
45
+ const CHEVRON_SIZE_PX = 16;
46
+ const HALF_TURN_DEG = 180;
47
+ const RevealClockContext = createContext(null);
48
+ /**
49
+ * Keys a time by its calendar day in a time zone.
50
+ * @param time - Epoch milliseconds.
51
+ * @param timeZone - Time zone of the day groups.
52
+ * @returns The day as `YYYY-MM-DD`.
53
+ */
54
+ function dayKey(time, timeZone) {
55
+ const parts = new Intl.DateTimeFormat("en-CA", {
56
+ timeZone,
57
+ year: "numeric",
58
+ month: "2-digit",
59
+ day: "2-digit"
60
+ }).formatToParts(new Date(time));
61
+ const get = (type) => parts.find((part) => part.type === type)?.value ?? "";
62
+ return `${get("year")}-${get("month")}-${get("day")}`;
63
+ }
64
+ /**
65
+ * The calendar day before a day key. It works on the date itself, so a daylight-saving change, where a day lasts 23 or
66
+ * 25 hours, never skips or repeats a day.
67
+ * @param day - A day as `YYYY-MM-DD`.
68
+ * @returns The previous day as `YYYY-MM-DD`.
69
+ */
70
+ function previousDayKey(day) {
71
+ const [year, month, date] = day.split("-").map(Number);
72
+ return new Date(Date.UTC(year, month - 1, date - 1)).toISOString().slice(0, DAY_KEY_LENGTH);
73
+ }
74
+ /**
75
+ * Relative time of a recent event, short for the row and long for screen readers.
76
+ * @param time - When the event happened, in epoch milliseconds.
77
+ * @param now - Reference time, in epoch milliseconds.
78
+ * @param copy - Timeline copy.
79
+ * @returns Short and long relative times.
80
+ */
81
+ function relativeTime(time, now, copy) {
82
+ const minutes = Math.max(0, Math.floor((now - time) / MILLISECONDS_PER_MINUTE));
83
+ if (minutes < 1) return {
84
+ short: copy.justNowShort,
85
+ long: copy.justNowLong
86
+ };
87
+ if (minutes < MINUTES_PER_HOUR) return {
88
+ short: copy.minutesShort(minutes),
89
+ long: copy.minutesLong(minutes)
90
+ };
91
+ const hours = Math.floor(minutes / MINUTES_PER_HOUR);
92
+ return {
93
+ short: copy.hoursShort(hours),
94
+ long: copy.hoursLong(hours)
95
+ };
96
+ }
97
+ /** The day's update count rolls digit by digit in the direction it moved. */
98
+ function RollingCount(t0) {
99
+ const $ = _c(8);
100
+ const { value, reduceMotion } = t0;
101
+ let t1;
102
+ if ($[0] !== value) {
103
+ t1 = {
104
+ value,
105
+ direction: 1
106
+ };
107
+ $[0] = value;
108
+ $[1] = t1;
109
+ } else {
110
+ t1 = $[1];
111
+ }
112
+ const [state, setState] = useState(t1);
113
+ if (state.value !== value) {
114
+ setState({
115
+ value,
116
+ direction: value > state.value ? 1 : -1
117
+ });
118
+ }
119
+ const t2 = String(value);
120
+ let t3;
121
+ if ($[2] !== t2) {
122
+ t3 = [...t2];
123
+ $[2] = t2;
124
+ $[3] = t3;
125
+ } else {
126
+ t3 = $[3];
127
+ }
128
+ const characters = t3;
129
+ let t4;
130
+ if ($[4] !== characters || $[5] !== reduceMotion || $[6] !== state.direction) {
131
+ t4 = /* @__PURE__ */ _jsx("span", {
132
+ "aria-hidden": "true",
133
+ className: "inline-flex",
134
+ children: characters.map((character, index) => /* @__PURE__ */ _jsx("span", {
135
+ className: "relative inline-flex overflow-clip [overflow-clip-margin:.15em]",
136
+ children: /* @__PURE__ */ _jsx(ArcTextSwap, {
137
+ text: character,
138
+ reduceMotion,
139
+ direction: state.direction,
140
+ className: "inline-flex",
141
+ lineClassName: "inline-block whitespace-nowrap"
142
+ })
143
+ }, characters.length - index))
144
+ });
145
+ $[4] = characters;
146
+ $[5] = reduceMotion;
147
+ $[6] = state.direction;
148
+ $[7] = t4;
149
+ } else {
150
+ t4 = $[7];
151
+ }
152
+ return t4;
153
+ }
154
+ /** A day's section. A new day opens from nothing and pushes the older days down; it clips only while it grows. */
155
+ function DaySection(t0) {
156
+ const $ = _c(17);
157
+ const { labelId, isFresh, reduceMotion, children } = t0;
158
+ const [isEntering, setIsEntering] = useState(isFresh);
159
+ const t1 = isEntering && "overflow-clip";
160
+ let t2;
161
+ if ($[0] !== t1) {
162
+ t2 = cn("relative", t1);
163
+ $[0] = t1;
164
+ $[1] = t2;
165
+ } else {
166
+ t2 = $[1];
167
+ }
168
+ let t3;
169
+ if ($[2] !== isFresh) {
170
+ t3 = isFresh ? {
171
+ height: 0,
172
+ opacity: 0
173
+ } : false;
174
+ $[2] = isFresh;
175
+ $[3] = t3;
176
+ } else {
177
+ t3 = $[3];
178
+ }
179
+ let t4;
180
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
181
+ t4 = {
182
+ height: "auto",
183
+ opacity: 1
184
+ };
185
+ $[4] = t4;
186
+ } else {
187
+ t4 = $[4];
188
+ }
189
+ const t5 = reduceMotion ? ARC_DURATION.instant : ARC_DURATION.exit;
190
+ let t6;
191
+ if ($[5] !== t5) {
192
+ t6 = {
193
+ height: 0,
194
+ opacity: 0,
195
+ transition: {
196
+ duration: t5,
197
+ ease: standardEase
198
+ }
199
+ };
200
+ $[5] = t5;
201
+ $[6] = t6;
202
+ } else {
203
+ t6 = $[6];
204
+ }
205
+ let t7;
206
+ if ($[7] !== reduceMotion) {
207
+ t7 = reduceMotion ? { duration: REDUCED_FADE_S } : {
208
+ height: ARC_SPRING_SMOOTH,
209
+ opacity: {
210
+ duration: ARC_DURATION.standard,
211
+ ease: enterEase
212
+ }
213
+ };
214
+ $[7] = reduceMotion;
215
+ $[8] = t7;
216
+ } else {
217
+ t7 = $[8];
218
+ }
219
+ let t8;
220
+ if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
221
+ t8 = () => setIsEntering(false);
222
+ $[9] = t8;
223
+ } else {
224
+ t8 = $[9];
225
+ }
226
+ let t9;
227
+ if ($[10] !== children || $[11] !== labelId || $[12] !== t2 || $[13] !== t3 || $[14] !== t6 || $[15] !== t7) {
228
+ t9 = /* @__PURE__ */ _jsx(motion.section, {
229
+ "data-slot": "timeline-day",
230
+ "aria-labelledby": labelId,
231
+ className: t2,
232
+ initial: t3,
233
+ animate: t4,
234
+ exit: t6,
235
+ transition: t7,
236
+ onAnimationComplete: t8,
237
+ children
238
+ });
239
+ $[10] = children;
240
+ $[11] = labelId;
241
+ $[12] = t2;
242
+ $[13] = t3;
243
+ $[14] = t6;
244
+ $[15] = t7;
245
+ $[16] = t9;
246
+ } else {
247
+ t9 = $[16];
248
+ }
249
+ return t9;
250
+ }
251
+ /** Marker colours: a quiet node for system events, tinted by tone. Portraits sit on a hairline ring. */
252
+ const MARKER_TONE_CLASS_NAMES = {
253
+ neutral: "bg-surface",
254
+ success: "bg-[color-mix(in_oklab,var(--success)_12%,var(--surface))] text-success",
255
+ danger: "bg-[color-mix(in_oklab,var(--danger)_12%,var(--surface))] text-danger"
256
+ };
257
+ /** One update: a marker on the line, the copy and time, and details that expand in place. */
258
+ function TimelineRow(t0) {
259
+ const $ = _c(99);
260
+ const { row, isLast, isExpanded, onToggle, timeLabel, timeFull } = t0;
261
+ const clock = useContext(RevealClockContext);
262
+ const { reduceMotion } = clock;
263
+ const detailId = useId();
264
+ const [revealDelay, setRevealDelay] = useState(null);
265
+ let t1;
266
+ if ($[0] !== clock.fresh || $[1] !== row.id) {
267
+ t1 = clock.fresh.has(row.id);
268
+ $[0] = clock.fresh;
269
+ $[1] = row.id;
270
+ $[2] = t1;
271
+ } else {
272
+ t1 = $[2];
273
+ }
274
+ const isFresh = t1;
275
+ const [isEntering, setIsEntering] = useState(isFresh);
276
+ const isShown = revealDelay !== null;
277
+ const delay = (revealDelay ?? 0) + (isFresh ? TIMELINE_FRESH_ROW_DELAY_S : 0);
278
+ let t2;
279
+ if ($[3] !== delay || $[4] !== reduceMotion) {
280
+ t2 = reduceMotion ? {
281
+ duration: 0,
282
+ opacity: { duration: REDUCED_FADE_S }
283
+ } : {
284
+ ...ARC_SPRING_MORPH,
285
+ visualDuration: MARKER_POP_DURATION_S,
286
+ bounce: MARKER_POP_BOUNCE,
287
+ delay,
288
+ opacity: {
289
+ duration: ARC_DURATION.fast,
290
+ ease: enterEase,
291
+ delay
292
+ }
293
+ };
294
+ $[3] = delay;
295
+ $[4] = reduceMotion;
296
+ $[5] = t2;
297
+ } else {
298
+ t2 = $[5];
299
+ }
300
+ const pop = t2;
301
+ let t3;
302
+ if ($[6] !== delay || $[7] !== reduceMotion) {
303
+ t3 = reduceMotion ? { duration: 0 } : {
304
+ ...ARC_SPRING_SMOOTH,
305
+ visualDuration: LINE_DRAW_DURATION_S,
306
+ delay: delay + LINE_DRAW_DELAY_S
307
+ };
308
+ $[6] = delay;
309
+ $[7] = reduceMotion;
310
+ $[8] = t3;
311
+ } else {
312
+ t3 = $[8];
313
+ }
314
+ const draw = t3;
315
+ const tone = row.avatar ? undefined : row.tone ?? "neutral";
316
+ let t4;
317
+ if ($[9] !== row.actor) {
318
+ t4 = row.actor ? /* @__PURE__ */ _jsx("span", {
319
+ className: "font-medium text-foreground",
320
+ children: row.actor
321
+ }) : null;
322
+ $[9] = row.actor;
323
+ $[10] = t4;
324
+ } else {
325
+ t4 = $[10];
326
+ }
327
+ const t5 = row.actor ? " " : "";
328
+ let t6;
329
+ if ($[11] !== row.title || $[12] !== t4 || $[13] !== t5) {
330
+ t6 = /* @__PURE__ */ _jsxs("span", {
331
+ "data-slot": "timeline-title",
332
+ className: "text-sm leading-5 text-text-secondary",
333
+ children: [
334
+ t4,
335
+ t5,
336
+ row.title
337
+ ]
338
+ });
339
+ $[11] = row.title;
340
+ $[12] = t4;
341
+ $[13] = t5;
342
+ $[14] = t6;
343
+ } else {
344
+ t6 = $[14];
345
+ }
346
+ let t7;
347
+ if ($[15] !== row.meta) {
348
+ t7 = row.meta ? /* @__PURE__ */ _jsx("span", {
349
+ className: "text-xs leading-(--leading-body) text-text-muted",
350
+ children: row.meta
351
+ }) : null;
352
+ $[15] = row.meta;
353
+ $[16] = t7;
354
+ } else {
355
+ t7 = $[16];
356
+ }
357
+ let t8;
358
+ if ($[17] !== t6 || $[18] !== t7) {
359
+ t8 = /* @__PURE__ */ _jsxs("span", {
360
+ className: "grid min-w-0 gap-0.5",
361
+ children: [t6, t7]
362
+ });
363
+ $[17] = t6;
364
+ $[18] = t7;
365
+ $[19] = t8;
366
+ } else {
367
+ t8 = $[19];
368
+ }
369
+ let t9;
370
+ if ($[20] !== row.time) {
371
+ t9 = new Date(row.time).toISOString();
372
+ $[20] = row.time;
373
+ $[21] = t9;
374
+ } else {
375
+ t9 = $[21];
376
+ }
377
+ let t10;
378
+ if ($[22] !== reduceMotion || $[23] !== timeLabel) {
379
+ t10 = /* @__PURE__ */ _jsx("span", {
380
+ "aria-hidden": "true",
381
+ children: /* @__PURE__ */ _jsx(ArcTextSwap, {
382
+ text: timeLabel,
383
+ reduceMotion,
384
+ className: "inline-flex",
385
+ lineClassName: "inline-block whitespace-nowrap"
386
+ })
387
+ });
388
+ $[22] = reduceMotion;
389
+ $[23] = timeLabel;
390
+ $[24] = t10;
391
+ } else {
392
+ t10 = $[24];
393
+ }
394
+ let t11;
395
+ if ($[25] !== timeFull) {
396
+ t11 = /* @__PURE__ */ _jsx("span", {
397
+ className: "sr-only",
398
+ children: timeFull
399
+ });
400
+ $[25] = timeFull;
401
+ $[26] = t11;
402
+ } else {
403
+ t11 = $[26];
404
+ }
405
+ let t12;
406
+ if ($[27] !== t10 || $[28] !== t11 || $[29] !== t9 || $[30] !== timeFull) {
407
+ t12 = /* @__PURE__ */ _jsxs("time", {
408
+ "data-slot": "timeline-time",
409
+ dateTime: t9,
410
+ title: timeFull,
411
+ className: "flex min-w-11 justify-end text-xs leading-5 whitespace-nowrap text-text-muted tabular-nums",
412
+ children: [t10, t11]
413
+ });
414
+ $[27] = t10;
415
+ $[28] = t11;
416
+ $[29] = t9;
417
+ $[30] = timeFull;
418
+ $[31] = t12;
419
+ } else {
420
+ t12 = $[31];
421
+ }
422
+ let t13;
423
+ if ($[32] !== t12 || $[33] !== t8) {
424
+ t13 = /* @__PURE__ */ _jsxs(_Fragment, { children: [t8, t12] });
425
+ $[32] = t12;
426
+ $[33] = t8;
427
+ $[34] = t13;
428
+ } else {
429
+ t13 = $[34];
430
+ }
431
+ const content = t13;
432
+ const t14 = isEntering && "overflow-clip";
433
+ let t15;
434
+ if ($[35] !== t14) {
435
+ t15 = cn("relative pl-[calc(var(--marker)+var(--space-2))]", t14);
436
+ $[35] = t14;
437
+ $[36] = t15;
438
+ } else {
439
+ t15 = $[36];
440
+ }
441
+ let t16;
442
+ if ($[37] !== isFresh) {
443
+ t16 = isFresh ? { height: 0 } : false;
444
+ $[37] = isFresh;
445
+ $[38] = t16;
446
+ } else {
447
+ t16 = $[38];
448
+ }
449
+ let t17;
450
+ if ($[39] === Symbol.for("react.memo_cache_sentinel")) {
451
+ t17 = { height: "auto" };
452
+ $[39] = t17;
453
+ } else {
454
+ t17 = $[39];
455
+ }
456
+ let t18;
457
+ let t19;
458
+ if ($[40] !== reduceMotion) {
459
+ t18 = {
460
+ height: 0,
461
+ opacity: 0,
462
+ transition: reduceMotion ? { duration: REDUCED_EXIT_S } : {
463
+ height: {
464
+ ...ARC_SPRING_SMOOTH,
465
+ visualDuration: ROW_EXIT_DURATION_S
466
+ },
467
+ opacity: { duration: ARC_DURATION.instant }
468
+ }
469
+ };
470
+ t19 = reduceMotion ? { duration: 0 } : ARC_SPRING_SMOOTH;
471
+ $[40] = reduceMotion;
472
+ $[41] = t18;
473
+ $[42] = t19;
474
+ } else {
475
+ t18 = $[41];
476
+ t19 = $[42];
477
+ }
478
+ let t20;
479
+ if ($[43] === Symbol.for("react.memo_cache_sentinel")) {
480
+ t20 = () => setIsEntering(false);
481
+ $[43] = t20;
482
+ } else {
483
+ t20 = $[43];
484
+ }
485
+ let t21;
486
+ if ($[44] !== clock) {
487
+ t21 = () => setRevealDelay((current) => current ?? clock.schedule());
488
+ $[44] = clock;
489
+ $[45] = t21;
490
+ } else {
491
+ t21 = $[45];
492
+ }
493
+ let t22;
494
+ if ($[46] === Symbol.for("react.memo_cache_sentinel")) {
495
+ t22 = {
496
+ once: true,
497
+ amount: TIMELINE_REVEAL_AMOUNT
498
+ };
499
+ $[46] = t22;
500
+ } else {
501
+ t22 = $[46];
502
+ }
503
+ const t23 = tone && MARKER_TONE_CLASS_NAMES[tone];
504
+ let t24;
505
+ if ($[47] !== t23) {
506
+ t24 = cn("absolute top-(--marker-top) left-0 z-1 grid size-(--marker) place-items-center rounded-(--radius-pill) text-text-secondary after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-[color-mix(in_oklab,var(--foreground)_10%,transparent)] after:content-[''] [&_svg]:size-3.5 [&_svg]:stroke-2", t23);
507
+ $[47] = t23;
508
+ $[48] = t24;
509
+ } else {
510
+ t24 = $[48];
511
+ }
512
+ let t25;
513
+ if ($[49] === Symbol.for("react.memo_cache_sentinel")) {
514
+ t25 = {
515
+ scale: MARKER_ENTER_SCALE,
516
+ opacity: 0
517
+ };
518
+ $[49] = t25;
519
+ } else {
520
+ t25 = $[49];
521
+ }
522
+ let t26;
523
+ if ($[50] !== isShown) {
524
+ t26 = isShown ? {
525
+ scale: 1,
526
+ opacity: 1
527
+ } : undefined;
528
+ $[50] = isShown;
529
+ $[51] = t26;
530
+ } else {
531
+ t26 = $[51];
532
+ }
533
+ let t27;
534
+ if ($[52] !== row.avatar || $[53] !== row.icon) {
535
+ t27 = row.avatar ? /* @__PURE__ */ _jsx("img", {
536
+ src: row.avatar,
537
+ alt: "",
538
+ width: TIMELINE_AVATAR_SIZE_PX,
539
+ height: TIMELINE_AVATAR_SIZE_PX,
540
+ decoding: "async",
541
+ className: "block size-full rounded-[inherit] object-cover"
542
+ }) : row.icon;
543
+ $[52] = row.avatar;
544
+ $[53] = row.icon;
545
+ $[54] = t27;
546
+ } else {
547
+ t27 = $[54];
548
+ }
549
+ let t28;
550
+ if ($[55] !== pop || $[56] !== t24 || $[57] !== t26 || $[58] !== t27 || $[59] !== tone) {
551
+ t28 = /* @__PURE__ */ _jsx(motion.span, {
552
+ "data-slot": "timeline-marker",
553
+ "data-tone": tone,
554
+ "aria-hidden": "true",
555
+ className: t24,
556
+ initial: t25,
557
+ animate: t26,
558
+ transition: pop,
559
+ children: t27
560
+ });
561
+ $[55] = pop;
562
+ $[56] = t24;
563
+ $[57] = t26;
564
+ $[58] = t27;
565
+ $[59] = tone;
566
+ $[60] = t28;
567
+ } else {
568
+ t28 = $[60];
569
+ }
570
+ let t29;
571
+ if ($[61] !== draw || $[62] !== isLast || $[63] !== isShown) {
572
+ t29 = !isLast ? /* @__PURE__ */ _jsx(motion.span, {
573
+ "data-slot": "timeline-line",
574
+ "aria-hidden": "true",
575
+ className: "absolute top-[calc(var(--marker-top)+var(--marker)+var(--line-gap))] bottom-[calc(var(--line-gap)-var(--marker-top))] left-[calc(var(--marker)/2_-_.5px)] w-px origin-top bg-border-strong",
576
+ initial: { scaleY: 0 },
577
+ animate: isShown ? { scaleY: 1 } : undefined,
578
+ transition: draw
579
+ }) : null;
580
+ $[61] = draw;
581
+ $[62] = isLast;
582
+ $[63] = isShown;
583
+ $[64] = t29;
584
+ } else {
585
+ t29 = $[64];
586
+ }
587
+ let t30;
588
+ if ($[65] !== isFresh || $[66] !== reduceMotion) {
589
+ t30 = isFresh ? reduceMotion ? { opacity: 0 } : {
590
+ opacity: 0,
591
+ y: FRESH_CONTENT_OFFSET_PX,
592
+ filter: `blur(${ARC_BLUR.soft}px)`
593
+ } : false;
594
+ $[65] = isFresh;
595
+ $[66] = reduceMotion;
596
+ $[67] = t30;
597
+ } else {
598
+ t30 = $[67];
599
+ }
600
+ let t31;
601
+ if ($[68] === Symbol.for("react.memo_cache_sentinel")) {
602
+ t31 = {
603
+ opacity: 1,
604
+ y: 0,
605
+ filter: "blur(0px)"
606
+ };
607
+ $[68] = t31;
608
+ } else {
609
+ t31 = $[68];
610
+ }
611
+ let t32;
612
+ if ($[69] !== reduceMotion) {
613
+ t32 = reduceMotion ? { duration: REDUCED_FADE_S } : {
614
+ y: ARC_SPRING_SMOOTH,
615
+ opacity: {
616
+ duration: ARC_DURATION.standard,
617
+ ease: enterEase,
618
+ delay: CONTENT_FADE_DELAY_S
619
+ },
620
+ filter: {
621
+ duration: ARC_DURATION.standard,
622
+ ease: enterEase,
623
+ delay: CONTENT_FADE_DELAY_S
624
+ }
625
+ };
626
+ $[69] = reduceMotion;
627
+ $[70] = t32;
628
+ } else {
629
+ t32 = $[70];
630
+ }
631
+ let t33;
632
+ if ($[71] !== content || $[72] !== detailId || $[73] !== isExpanded || $[74] !== onToggle || $[75] !== reduceMotion || $[76] !== row.detail) {
633
+ t33 = row.detail ? /* @__PURE__ */ _jsxs("button", {
634
+ type: "button",
635
+ "data-timeline-trigger": "",
636
+ "aria-expanded": isExpanded,
637
+ "aria-controls": isExpanded ? detailId : undefined,
638
+ onClick: onToggle,
639
+ className: cn("grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-x-(--space-3) rounded-(--radius-control) border-0 bg-transparent px-(--space-3) py-3 text-left [font:inherit] text-inherit [-webkit-tap-highlight-color:transparent] transition-[background-color] duration-(--duration-fast) ease-(--ease-standard) max-[420px]:gap-x-(--space-2) max-[420px]:px-(--space-2) motion-reduce:transition-none", "cursor-pointer grid-cols-[minmax(0,1fr)_auto_16px] hover-fine:bg-surface-muted active:bg-surface-muted"),
640
+ children: [content, /* @__PURE__ */ _jsx(motion.span, {
641
+ "aria-hidden": "true",
642
+ className: "grid h-5 place-items-center text-text-muted",
643
+ initial: false,
644
+ animate: { rotate: isExpanded ? HALF_TURN_DEG : 0 },
645
+ transition: reduceMotion ? { duration: 0 } : ARC_SPRING_SNAPPY,
646
+ children: /* @__PURE__ */ _jsx(ChevronDown, {
647
+ size: CHEVRON_SIZE_PX,
648
+ strokeWidth: 1.75
649
+ })
650
+ })]
651
+ }) : /* @__PURE__ */ _jsx("div", {
652
+ className: "grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-x-(--space-3) rounded-(--radius-control) border-0 bg-transparent px-(--space-3) py-3 text-left [font:inherit] text-inherit [-webkit-tap-highlight-color:transparent] transition-[background-color] duration-(--duration-fast) ease-(--ease-standard) max-[420px]:gap-x-(--space-2) max-[420px]:px-(--space-2) motion-reduce:transition-none",
653
+ children: content
654
+ });
655
+ $[71] = content;
656
+ $[72] = detailId;
657
+ $[73] = isExpanded;
658
+ $[74] = onToggle;
659
+ $[75] = reduceMotion;
660
+ $[76] = row.detail;
661
+ $[77] = t33;
662
+ } else {
663
+ t33 = $[77];
664
+ }
665
+ let t34;
666
+ if ($[78] !== detailId || $[79] !== isExpanded || $[80] !== reduceMotion || $[81] !== row.detail) {
667
+ t34 = isExpanded && row.detail ? /* @__PURE__ */ _jsx(motion.div, {
668
+ id: detailId,
669
+ "data-slot": "timeline-detail",
670
+ className: "overflow-clip [overflow-clip-margin:4px]",
671
+ initial: { height: 0 },
672
+ animate: { height: "auto" },
673
+ exit: {
674
+ height: 0,
675
+ transition: reduceMotion ? { duration: 0 } : {
676
+ ...ARC_SPRING_SMOOTH,
677
+ visualDuration: DETAIL_COLLAPSE_DURATION_S
678
+ }
679
+ },
680
+ transition: reduceMotion ? { duration: 0 } : ARC_SPRING_SMOOTH,
681
+ children: /* @__PURE__ */ _jsx(motion.div, {
682
+ className: "px-(--space-3) pb-(--space-4) text-sm leading-(--leading-body) text-text-secondary max-[420px]:px-(--space-2)",
683
+ initial: reduceMotion ? { opacity: 0 } : {
684
+ opacity: 0,
685
+ y: DETAIL_OFFSET_PX,
686
+ filter: `blur(${ARC_BLUR.soft}px)`
687
+ },
688
+ animate: {
689
+ opacity: 1,
690
+ y: 0,
691
+ filter: "blur(0px)"
692
+ },
693
+ exit: {
694
+ opacity: 0,
695
+ transition: {
696
+ duration: ARC_DURATION.instant,
697
+ ease: standardEase
698
+ }
699
+ },
700
+ transition: reduceMotion ? { duration: REDUCED_FADE_S } : {
701
+ duration: ARC_DURATION.standard,
702
+ ease: enterEase,
703
+ delay: DETAIL_FADE_DELAY_S
704
+ },
705
+ children: row.detail
706
+ })
707
+ }, "detail") : null;
708
+ $[78] = detailId;
709
+ $[79] = isExpanded;
710
+ $[80] = reduceMotion;
711
+ $[81] = row.detail;
712
+ $[82] = t34;
713
+ } else {
714
+ t34 = $[82];
715
+ }
716
+ let t35;
717
+ if ($[83] !== t34) {
718
+ t35 = /* @__PURE__ */ _jsx(AnimatePresence, {
719
+ initial: false,
720
+ children: t34
721
+ });
722
+ $[83] = t34;
723
+ $[84] = t35;
724
+ } else {
725
+ t35 = $[84];
726
+ }
727
+ let t36;
728
+ if ($[85] !== t30 || $[86] !== t32 || $[87] !== t33 || $[88] !== t35) {
729
+ t36 = /* @__PURE__ */ _jsxs(motion.div, {
730
+ className: "min-w-0",
731
+ initial: t30,
732
+ animate: t31,
733
+ transition: t32,
734
+ children: [t33, t35]
735
+ });
736
+ $[85] = t30;
737
+ $[86] = t32;
738
+ $[87] = t33;
739
+ $[88] = t35;
740
+ $[89] = t36;
741
+ } else {
742
+ t36 = $[89];
743
+ }
744
+ let t37;
745
+ if ($[90] !== t15 || $[91] !== t16 || $[92] !== t18 || $[93] !== t19 || $[94] !== t21 || $[95] !== t28 || $[96] !== t29 || $[97] !== t36) {
746
+ t37 = /* @__PURE__ */ _jsxs(motion.li, {
747
+ "data-slot": "timeline-item",
748
+ className: t15,
749
+ initial: t16,
750
+ animate: t17,
751
+ exit: t18,
752
+ transition: t19,
753
+ onAnimationComplete: t20,
754
+ onViewportEnter: t21,
755
+ viewport: t22,
756
+ children: [
757
+ t28,
758
+ t29,
759
+ t36
760
+ ]
761
+ });
762
+ $[90] = t15;
763
+ $[91] = t16;
764
+ $[92] = t18;
765
+ $[93] = t19;
766
+ $[94] = t21;
767
+ $[95] = t28;
768
+ $[96] = t29;
769
+ $[97] = t36;
770
+ $[98] = t37;
771
+ } else {
772
+ t37 = $[98];
773
+ }
774
+ return t37;
775
+ }
776
+ /**
777
+ * A vertical activity feed grouped by day with pinned day labels and rows that expand in place.
778
+ * @param props - Events, reference time, accessible name, formatting, scrolling, copy and native div attributes.
779
+ * @returns The labelled feed region.
780
+ */
781
+ function Timeline({ events, now, label, timeZone = TIMELINE_DEFAULT_TIME_ZONE, locale = TIMELINE_DEFAULT_LOCALE, maxHeight, scrollToNew = true, defaultExpanded = [], headingLevel = TIMELINE_DEFAULT_HEADING_LEVEL, messages, className, ...props }) {
782
+ const copy = {
783
+ ...TIMELINE_DEFAULT_MESSAGES,
784
+ ...messages
785
+ };
786
+ const prefersReducedMotion = usePrefersReducedMotion();
787
+ const isHydrated = useIsHydrated();
788
+ const reduceMotion = isHydrated && prefersReducedMotion;
789
+ const id = useId();
790
+ const scrollerRef = useRef(null);
791
+ const trackRef = useRef(null);
792
+ const [expanded, setExpanded] = useState(() => new Set(defaultExpanded));
793
+ const ids = events.map((event) => event.id).join("|");
794
+ const [known, setKnown] = useState(() => ({
795
+ ids,
796
+ set: new Set(events.map((event) => event.id)),
797
+ fresh: new Set(),
798
+ announcement: "",
799
+ announcementCount: 0
800
+ }));
801
+ if (known.ids !== ids) {
802
+ const added = events.filter((event) => !known.set.has(event.id));
803
+ setKnown({
804
+ ids,
805
+ set: new Set(events.map((event) => event.id)),
806
+ fresh: new Set([...known.fresh, ...added.map((event) => event.id)]),
807
+ announcement: added.length ? copy.newUpdates(added.map((event) => [event.actor, event.title].filter(Boolean).join(" ")).join(". ")) : known.announcement,
808
+ announcementCount: known.announcementCount + (added.length ? 1 : 0)
809
+ });
810
+ }
811
+ const scrolls = maxHeight !== undefined;
812
+ useEffect(() => {
813
+ const scroller = scrollerRef.current;
814
+ const track = trackRef.current;
815
+ if (!scrolls || !scroller || !track || typeof ResizeObserver === "undefined") return;
816
+ const update = () => {
817
+ if (scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop > 2) scroller.dataset.more = "";
818
+ else delete scroller.dataset.more;
819
+ };
820
+ update();
821
+ scroller.addEventListener("scroll", update, { passive: true });
822
+ const observer = new ResizeObserver(update);
823
+ observer.observe(scroller);
824
+ observer.observe(track);
825
+ return () => {
826
+ scroller.removeEventListener("scroll", update);
827
+ observer.disconnect();
828
+ };
829
+ }, [scrolls]);
830
+ const newestId = events.reduce((latest, event) => !latest || new Date(event.at).getTime() > new Date(latest.at).getTime() ? event : latest, null)?.id;
831
+ const scrollOptions = useRef({
832
+ scrollToNew,
833
+ reduceMotion
834
+ });
835
+ useEffect(() => {
836
+ scrollOptions.current = {
837
+ scrollToNew,
838
+ reduceMotion
839
+ };
840
+ });
841
+ const shownNewestId = useRef(newestId);
842
+ useEffect(() => {
843
+ if (shownNewestId.current === newestId) return;
844
+ shownNewestId.current = newestId;
845
+ const scroller = scrollerRef.current;
846
+ if (!scrollOptions.current.scrollToNew || !scroller || scroller.scrollTop < 1) return;
847
+ scroller.scrollTo({
848
+ top: 0,
849
+ behavior: scrollOptions.current.reduceMotion ? "auto" : "smooth"
850
+ });
851
+ }, [newestId]);
852
+ const [initialDays] = useState(() => new Set(events.map((event) => dayKey(new Date(event.at).getTime(), timeZone))));
853
+ const { today: todayLabel, yesterday: yesterdayLabel } = copy;
854
+ const todayKey = dayKey(now, timeZone);
855
+ const groups = useMemo(() => {
856
+ const today = dayKey(now, timeZone);
857
+ const yesterday = previousDayKey(today);
858
+ const heading = new Intl.DateTimeFormat(locale, {
859
+ weekday: "long",
860
+ month: "long",
861
+ day: "numeric",
862
+ timeZone
863
+ });
864
+ const byDay = new Map();
865
+ events.map((event) => ({
866
+ ...event,
867
+ time: new Date(event.at).getTime()
868
+ })).sort((first, second) => second.time - first.time).forEach((event) => {
869
+ const day = dayKey(event.time, timeZone);
870
+ byDay.set(day, [...byDay.get(day) ?? [], {
871
+ ...event,
872
+ day
873
+ }]);
874
+ });
875
+ return [...byDay].map(([day, rows]) => ({
876
+ day,
877
+ rows,
878
+ label: day === today ? todayLabel : day === yesterday ? yesterdayLabel : heading.format(new Date(rows[0].time))
879
+ }));
880
+ }, [
881
+ events,
882
+ now,
883
+ timeZone,
884
+ locale,
885
+ todayLabel,
886
+ yesterdayLabel
887
+ ]);
888
+ const formats = useMemo(() => ({
889
+ clock: new Intl.DateTimeFormat(locale, {
890
+ hour: "numeric",
891
+ minute: "2-digit",
892
+ timeZone
893
+ }),
894
+ full: new Intl.DateTimeFormat(locale, {
895
+ weekday: "long",
896
+ month: "long",
897
+ day: "numeric",
898
+ hour: "numeric",
899
+ minute: "2-digit",
900
+ timeZone
901
+ })
902
+ }), [locale, timeZone]);
903
+ const nextReveal = useRef(0);
904
+ const schedule = useCallback(() => {
905
+ if (reduceMotion) return 0;
906
+ const current = performance.now() / MILLISECONDS_PER_SECOND;
907
+ const start = Math.min(Math.max(current, nextReveal.current), current + TIMELINE_REVEAL_QUEUE_LIMIT_S);
908
+ nextReveal.current = start + TIMELINE_REVEAL_STEP_S;
909
+ return start - current;
910
+ }, [reduceMotion]);
911
+ const clock = useMemo(() => ({
912
+ schedule,
913
+ reduceMotion,
914
+ fresh: known.fresh
915
+ }), [
916
+ schedule,
917
+ reduceMotion,
918
+ known.fresh
919
+ ]);
920
+ const toggle = (eventId) => {
921
+ setExpanded((current) => {
922
+ const next = new Set(current);
923
+ if (next.has(eventId)) next.delete(eventId);
924
+ else next.add(eventId);
925
+ return next;
926
+ });
927
+ };
928
+ const onKeyDown = (event) => {
929
+ if (![
930
+ "ArrowDown",
931
+ "ArrowUp",
932
+ "Home",
933
+ "End"
934
+ ].includes(event.key)) return;
935
+ const triggers = [...scrollerRef.current?.querySelectorAll("[data-timeline-trigger]") ?? []];
936
+ const index = triggers.indexOf(document.activeElement);
937
+ if (index < 0) return;
938
+ event.preventDefault();
939
+ const next = event.key === "Home" ? 0 : event.key === "End" ? triggers.length - 1 : Math.min(Math.max(index + (event.key === "ArrowDown" ? 1 : -1), 0), triggers.length - 1);
940
+ triggers[next]?.focus();
941
+ };
942
+ return /* @__PURE__ */ _jsx(RevealClockContext.Provider, {
943
+ value: clock,
944
+ children: /* @__PURE__ */ _jsxs("div", {
945
+ "data-slot": "timeline",
946
+ role: "region",
947
+ "aria-label": label,
948
+ className: cn("relative grid min-w-0 text-foreground [--line-gap:4px] [--marker-top:8px] [--marker:28px]", className),
949
+ ...props,
950
+ children: [/* @__PURE__ */ _jsx("div", {
951
+ ref: scrollerRef,
952
+ "data-slot": "timeline-scroller",
953
+ "data-scrolls": scrolls || undefined,
954
+ className: "min-w-0 [overflow-anchor:none] data-scrolls:max-h-(--timeline-height) data-scrolls:overflow-y-auto data-scrolls:overscroll-contain data-scrolls:[scrollbar-color:var(--border-strong)_transparent] data-scrolls:[scrollbar-width:thin] data-more:[mask-image:linear-gradient(to_bottom,#000_calc(100%_-_36px),transparent)]",
955
+ style: scrolls ? { "--timeline-height": typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight } : undefined,
956
+ onKeyDown,
957
+ children: /* @__PURE__ */ _jsx("div", {
958
+ ref: trackRef,
959
+ children: /* @__PURE__ */ _jsx(AnimatePresence, { children: groups.map((group) => {
960
+ const labelId = `${id}-${group.day}`;
961
+ const noun = copy.updates(group.rows.length);
962
+ return /* @__PURE__ */ _jsxs(DaySection, {
963
+ labelId,
964
+ isFresh: !initialDays.has(group.day),
965
+ reduceMotion,
966
+ children: [/* @__PURE__ */ _jsxs("div", {
967
+ id: labelId,
968
+ role: "heading",
969
+ "aria-level": headingLevel,
970
+ className: "sticky top-0 z-2 flex items-baseline justify-between gap-(--space-3) bg-[var(--timeline-surface,var(--surface))] py-(--space-2) pr-(--space-3) text-sm leading-(--leading-body) font-medium text-foreground",
971
+ children: [
972
+ /* @__PURE__ */ _jsx("span", { children: group.label }),
973
+ /* @__PURE__ */ _jsxs("span", {
974
+ "aria-hidden": "true",
975
+ className: "inline-flex items-baseline text-xs font-normal whitespace-pre text-text-muted tabular-nums",
976
+ children: [/* @__PURE__ */ _jsx(RollingCount, {
977
+ value: group.rows.length,
978
+ reduceMotion
979
+ }), ` ${noun}`]
980
+ }),
981
+ /* @__PURE__ */ _jsx("span", {
982
+ className: "sr-only",
983
+ children: `, ${group.rows.length} ${noun}`
984
+ })
985
+ ]
986
+ }), /* @__PURE__ */ _jsx("ol", {
987
+ className: "m-0 mb-(--space-3) list-none p-0",
988
+ children: /* @__PURE__ */ _jsx(AnimatePresence, { children: group.rows.map((row, index) => {
989
+ const isRecent = group.day === todayKey && now - row.time < TIMELINE_RELATIVE_WINDOW_HOURS * MILLISECONDS_PER_HOUR;
990
+ const full = formats.full.format(new Date(row.time));
991
+ const relative = isRecent ? relativeTime(row.time, now, copy) : null;
992
+ return /* @__PURE__ */ _jsx(TimelineRow, {
993
+ row,
994
+ isLast: index === group.rows.length - 1,
995
+ isExpanded: expanded.has(row.id),
996
+ onToggle: () => toggle(row.id),
997
+ timeLabel: relative ? relative.short : formats.clock.format(new Date(row.time)),
998
+ timeFull: relative ? `${relative.long}, ${full}` : full
999
+ }, row.id);
1000
+ }) })
1001
+ })]
1002
+ }, group.day);
1003
+ }) })
1004
+ })
1005
+ }), /* @__PURE__ */ _jsx("span", {
1006
+ className: "sr-only",
1007
+ role: "status",
1008
+ children: /* @__PURE__ */ _jsx("span", { children: known.announcement }, known.announcementCount)
1009
+ })]
1010
+ })
1011
+ });
1012
+ }
1013
+ export { Timeline };