@repraze/kernelkit 0.1.0-alpha.4 → 0.1.0-alpha.6

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 (125) hide show
  1. package/dist/components/alert-dialog.d.ts.map +1 -1
  2. package/dist/components/alert-dialog.js +4 -1
  3. package/dist/components/alert-dialog.js.map +1 -1
  4. package/dist/components/alert.js +3 -3
  5. package/dist/components/alert.js.map +1 -1
  6. package/dist/components/badge.js +1 -1
  7. package/dist/components/badge.js.map +1 -1
  8. package/dist/components/button.js +1 -1
  9. package/dist/components/button.js.map +1 -1
  10. package/dist/components/calendar.d.ts +8 -1
  11. package/dist/components/calendar.d.ts.map +1 -1
  12. package/dist/components/calendar.js +33 -11
  13. package/dist/components/calendar.js.map +1 -1
  14. package/dist/components/callout.js +2 -2
  15. package/dist/components/callout.js.map +1 -1
  16. package/dist/components/combobox.js +1 -1
  17. package/dist/components/combobox.js.map +1 -1
  18. package/dist/components/date-picker.d.ts +11 -5
  19. package/dist/components/date-picker.d.ts.map +1 -1
  20. package/dist/components/date-picker.js +29 -7
  21. package/dist/components/date-picker.js.map +1 -1
  22. package/dist/components/description-list.d.ts +26 -0
  23. package/dist/components/description-list.d.ts.map +1 -0
  24. package/dist/components/description-list.js +59 -0
  25. package/dist/components/description-list.js.map +1 -0
  26. package/dist/components/locale.d.ts +19 -0
  27. package/dist/components/locale.d.ts.map +1 -0
  28. package/dist/components/locale.js +54 -0
  29. package/dist/components/locale.js.map +1 -0
  30. package/dist/components/pager.d.ts +1 -1
  31. package/dist/components/pager.d.ts.map +1 -1
  32. package/dist/components/pager.js +4 -4
  33. package/dist/components/pager.js.map +1 -1
  34. package/dist/components/popover.d.ts +1 -1
  35. package/dist/components/popover.d.ts.map +1 -1
  36. package/dist/components/popover.js +2 -2
  37. package/dist/components/popover.js.map +1 -1
  38. package/dist/components/progress.d.ts +19 -6
  39. package/dist/components/progress.d.ts.map +1 -1
  40. package/dist/components/progress.js +18 -3
  41. package/dist/components/progress.js.map +1 -1
  42. package/dist/components/search-trigger.d.ts +14 -4
  43. package/dist/components/search-trigger.d.ts.map +1 -1
  44. package/dist/components/search-trigger.js +25 -4
  45. package/dist/components/search-trigger.js.map +1 -1
  46. package/dist/components/shimmer.d.ts +13 -0
  47. package/dist/components/shimmer.d.ts.map +1 -0
  48. package/dist/components/shimmer.js +21 -0
  49. package/dist/components/shimmer.js.map +1 -0
  50. package/dist/components/spinner.d.ts +28 -6
  51. package/dist/components/spinner.d.ts.map +1 -1
  52. package/dist/components/spinner.js +66 -14
  53. package/dist/components/spinner.js.map +1 -1
  54. package/dist/components/stepper.d.ts +64 -0
  55. package/dist/components/stepper.d.ts.map +1 -0
  56. package/dist/components/stepper.js +167 -0
  57. package/dist/components/stepper.js.map +1 -0
  58. package/dist/components/tag-input.d.ts +47 -0
  59. package/dist/components/tag-input.d.ts.map +1 -0
  60. package/dist/components/tag-input.js +211 -0
  61. package/dist/components/tag-input.js.map +1 -0
  62. package/dist/components/time-input.d.ts +75 -0
  63. package/dist/components/time-input.d.ts.map +1 -0
  64. package/dist/components/time-input.js +341 -0
  65. package/dist/components/time-input.js.map +1 -0
  66. package/dist/components/time-select.d.ts +49 -0
  67. package/dist/components/time-select.d.ts.map +1 -0
  68. package/dist/components/time-select.js +178 -0
  69. package/dist/components/time-select.js.map +1 -0
  70. package/dist/components/timeline.d.ts +41 -0
  71. package/dist/components/timeline.d.ts.map +1 -0
  72. package/dist/components/timeline.js +83 -0
  73. package/dist/components/timeline.js.map +1 -0
  74. package/dist/components/toast.d.ts.map +1 -1
  75. package/dist/components/toast.js +4 -2
  76. package/dist/components/toast.js.map +1 -1
  77. package/dist/components/tree.d.ts +87 -0
  78. package/dist/components/tree.d.ts.map +1 -0
  79. package/dist/components/tree.js +516 -0
  80. package/dist/components/tree.js.map +1 -0
  81. package/dist/hooks/use-time-format.d.ts +23 -0
  82. package/dist/hooks/use-time-format.d.ts.map +1 -0
  83. package/dist/hooks/use-time-format.js +52 -0
  84. package/dist/hooks/use-time-format.js.map +1 -0
  85. package/dist/lib/color.d.ts +5 -1
  86. package/dist/lib/color.d.ts.map +1 -1
  87. package/dist/lib/color.js +14 -2
  88. package/dist/lib/color.js.map +1 -1
  89. package/dist/lib/recipes.d.ts +11 -6
  90. package/dist/lib/recipes.d.ts.map +1 -1
  91. package/dist/lib/recipes.js +13 -7
  92. package/dist/lib/recipes.js.map +1 -1
  93. package/dist/site/suite-header.d.ts +4 -1
  94. package/dist/site/suite-header.d.ts.map +1 -1
  95. package/dist/site/suite-header.js +7 -2
  96. package/dist/site/suite-header.js.map +1 -1
  97. package/package.json +1 -1
  98. package/src/components/alert-dialog.tsx +4 -1
  99. package/src/components/alert.tsx +3 -3
  100. package/src/components/badge.tsx +1 -1
  101. package/src/components/button.tsx +1 -1
  102. package/src/components/calendar.tsx +48 -4
  103. package/src/components/callout.tsx +2 -2
  104. package/src/components/combobox.tsx +1 -1
  105. package/src/components/date-picker.tsx +43 -9
  106. package/src/components/description-list.tsx +107 -0
  107. package/src/components/locale.tsx +67 -0
  108. package/src/components/pager.tsx +8 -8
  109. package/src/components/popover.tsx +3 -1
  110. package/src/components/progress.tsx +70 -4
  111. package/src/components/search-trigger.tsx +40 -7
  112. package/src/components/shimmer.tsx +36 -0
  113. package/src/components/spinner.tsx +126 -23
  114. package/src/components/stepper.tsx +346 -0
  115. package/src/components/tag-input.tsx +345 -0
  116. package/src/components/time-input.tsx +578 -0
  117. package/src/components/time-select.tsx +291 -0
  118. package/src/components/timeline.tsx +190 -0
  119. package/src/components/toast.tsx +4 -4
  120. package/src/components/tree.tsx +832 -0
  121. package/src/hooks/use-time-format.ts +72 -0
  122. package/src/lib/color.ts +19 -2
  123. package/src/lib/recipes.ts +13 -7
  124. package/src/site/suite-header.tsx +25 -4
  125. package/src/styles/kernelkit.css +56 -0
@@ -0,0 +1,291 @@
1
+ "use client";
2
+
3
+ import {
4
+ Combobox,
5
+ ComboboxContent,
6
+ ComboboxEmpty,
7
+ ComboboxInput,
8
+ ComboboxItem,
9
+ ComboboxList,
10
+ } from "@repraze/kernelkit/components/combobox";
11
+ import {type HourCyclePreference, useTimeFormat} from "@repraze/kernelkit/hooks/use-time-format";
12
+ import {cva} from "class-variance-authority";
13
+ import {cn} from "cn";
14
+ import * as React from "react";
15
+
16
+ /* ------------------------------------------------------------------------------------------------
17
+ * Time helpers (minutes since midnight)
18
+ * ----------------------------------------------------------------------------------------------*/
19
+
20
+ const pad = (n: number) => String(n).padStart(2, "0");
21
+
22
+ function toMinutes(value: string | null | undefined): number | null {
23
+ const m = value ? /^(\d{1,2}):(\d{2})/.exec(value) : null;
24
+ if (!m) {
25
+ return null;
26
+ }
27
+ const h = Number(m[1]);
28
+ const min = Number(m[2]);
29
+ return h > 23 || min > 59 ? null : h * 60 + min;
30
+ }
31
+
32
+ const toValue = (minutes: number) => `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;
33
+
34
+ function formatTime(minutes: number, cycle: 12 | 24) {
35
+ const h = Math.floor(minutes / 60);
36
+ const m = pad(minutes % 60);
37
+ return cycle === 24 ? `${pad(h)}:${m}` : `${h % 12 || 12}:${m} ${h < 12 ? "AM" : "PM"}`;
38
+ }
39
+
40
+ function formatDuration(minutes: number) {
41
+ if (minutes < 60) {
42
+ return `${minutes} min`;
43
+ }
44
+ const h = Math.floor(minutes / 60);
45
+ const m = minutes % 60;
46
+ return m ? `${h} h ${m} min` : `${h} h`;
47
+ }
48
+
49
+ /**
50
+ * Lenient match, like Google Calendar's time list: "9" → 09:xx and 9 PM, "930" / "9:3" → 09:30,
51
+ * "9p" → 21:xx, "21" → 21:xx. Returns the exact time when the query is a complete one ("10:10", "7:05pm").
52
+ */
53
+ function matchTime(query: string, cycle: 12 | 24 = 24) {
54
+ const q = query
55
+ .toLowerCase()
56
+ .replace(/\s+/g, "")
57
+ .replace(/\.(?=\d)/, ":");
58
+ const meridiem = /(a|am|p|pm)$/.exec(q)?.[1]?.[0] as "a" | "p" | undefined;
59
+ const body = meridiem ? q.replace(/(a|am|p|pm)$/, "") : q;
60
+ if (!/^\d{0,2}(:\d{0,2})?$|^\d{3,4}$/.test(body) || (!body && !meridiem)) {
61
+ return {test: () => false, exact: null as number | null};
62
+ }
63
+ let hourText: string;
64
+ let minuteText: string;
65
+ if (body.includes(":")) {
66
+ [hourText = "", minuteText = ""] = body.split(":");
67
+ } else if (body.length > 2) {
68
+ hourText = body.slice(0, body.length - 2);
69
+ minuteText = body.slice(-2);
70
+ } else {
71
+ hourText = body;
72
+ minuteText = "";
73
+ }
74
+
75
+ const test = (minutes: number) => {
76
+ const h24 = Math.floor(minutes / 60);
77
+ const h12 = h24 % 12 || 12;
78
+ const pm = h24 >= 12;
79
+ if (meridiem && (meridiem === "p") !== pm) {
80
+ return false;
81
+ }
82
+ // 24h display: hours are read as 24h ("9" is 09:xx) unless a meridiem is typed
83
+ const hourOk =
84
+ !hourText ||
85
+ (meridiem || cycle === 12
86
+ ? Number(hourText) === h12 || (!meridiem && Number(hourText) === h24)
87
+ : Number(hourText) === h24);
88
+ return hourOk && pad(minutes % 60).startsWith(minuteText);
89
+ };
90
+
91
+ // a complete time typed off the grid ("10:10") is offered as its own option
92
+ let exact: number | null = null;
93
+ const h = Number(hourText);
94
+ if (hourText && minuteText.length === 2) {
95
+ const m = Number(minuteText);
96
+ const h24 = meridiem ? (h % 12) + (meridiem === "p" ? 12 : 0) : h;
97
+ if (m < 60 && h24 < 24 && (!meridiem || (h >= 1 && h <= 12))) {
98
+ exact = h24 * 60 + m;
99
+ }
100
+ }
101
+ return {test, exact};
102
+ }
103
+
104
+ /* ------------------------------------------------------------------------------------------------
105
+ * TimeSelect
106
+ * ----------------------------------------------------------------------------------------------*/
107
+
108
+ const timeSelectVariants = cva(
109
+ "w-36 font-normal *:data-[slot=input-group-control]:font-mono *:data-[slot=input-group-control]:tabular-nums",
110
+ {
111
+ variants: {
112
+ size: {
113
+ sm: "h-7 rounded-md text-[0.8rem]",
114
+ default: "h-8",
115
+ lg: "h-9",
116
+ },
117
+ },
118
+ defaultVariants: {size: "default"},
119
+ },
120
+ );
121
+
122
+ interface TimeSelectProps {
123
+ /** 24-hour `"HH:mm"`, or `null`. */
124
+ value?: string | null;
125
+ defaultValue?: string | null;
126
+ onValueChange?: (value: string | null) => void;
127
+ /** Minutes between slots. */
128
+ step?: number;
129
+ /** First slot, `"HH:mm"`. */
130
+ min?: string;
131
+ /** Last slot, `"HH:mm"`. */
132
+ max?: string;
133
+ /** How times are shown; `"auto"` follows the user's locale. The value stays 24-hour. */
134
+ hourCycle?: HourCyclePreference;
135
+ /** BCP 47 locale for `"auto"` and the labels ("9:30 AM", "午前9:30"); defaults to the browser's. */
136
+ locale?: string;
137
+ /** A start time: slots begin there and show their duration, "(1 h 30 min)", like an end-time list. */
138
+ durationFrom?: string | null;
139
+ placeholder?: string;
140
+ size?: "sm" | "default" | "lg";
141
+ disabled?: boolean;
142
+ "aria-invalid"?: boolean;
143
+ "aria-label"?: string;
144
+ id?: string;
145
+ name?: string;
146
+ className?: string;
147
+ }
148
+
149
+ /**
150
+ * A time from a list of slots, Google Calendar style: open it and pick, or type a few characters
151
+ * ("9", "930", "9p") to filter. A complete time typed off the grid ("10:10") is offered too.
152
+ */
153
+ function TimeSelect({
154
+ value,
155
+ defaultValue = null,
156
+ onValueChange,
157
+ step = 30,
158
+ min = "00:00",
159
+ max = "23:59",
160
+ hourCycle: hourCyclePreference = "auto",
161
+ locale,
162
+ durationFrom,
163
+ placeholder: placeholderProp,
164
+ size = "default",
165
+ disabled,
166
+ id,
167
+ name,
168
+ className,
169
+ ...aria
170
+ }: TimeSelectProps) {
171
+ const controlled = value !== undefined;
172
+ const [inner, setInner] = React.useState<string | null>(defaultValue);
173
+ const current = controlled ? value : inner;
174
+ const currentMin = toMinutes(current);
175
+
176
+ const set = (next: string | null) => {
177
+ if (!controlled) {
178
+ setInner(next);
179
+ }
180
+ onValueChange?.(next);
181
+ };
182
+
183
+ const from = toMinutes(durationFrom);
184
+ const slots = React.useMemo(() => {
185
+ const lo = Math.max(toMinutes(min) ?? 0, from ?? 0);
186
+ const hi = toMinutes(max) ?? 23 * 60 + 59;
187
+ const s = Math.max(1, step);
188
+ const list: number[] = [];
189
+ // durationFrom: the grid starts at the start time; otherwise it is aligned on midnight
190
+ for (let t = from !== null ? lo : Math.ceil(lo / s) * s; t <= hi; t += s) {
191
+ list.push(t);
192
+ }
193
+ return list;
194
+ }, [min, max, step, from]);
195
+
196
+ const fmt = useTimeFormat(hourCyclePreference, locale);
197
+ const hourCycle = fmt.hourCycle;
198
+ const placeholder = placeholderProp ?? (hourCycle === 12 ? "--:-- --" : "--:--");
199
+ const label = fmt.format;
200
+
201
+ // filter only once the user types: opening on a picked time lists every slot, scrolled to it
202
+ const [input, setInput] = React.useState(currentMin === null ? "" : label(currentMin));
203
+ const [query, setQuery] = React.useState<string | null>(null);
204
+ // re-label when the value changes or the locale's format arrives after hydration
205
+ const seenKey = `${current}|${hourCycle}|${fmt.am}|${fmt.pm}`;
206
+ const [seen, setSeen] = React.useState(seenKey);
207
+ if (seenKey !== seen) {
208
+ setSeen(seenKey);
209
+ setInput(currentMin === null ? "" : label(currentMin));
210
+ }
211
+
212
+ const items = React.useMemo(() => {
213
+ const base =
214
+ currentMin !== null && !slots.includes(currentMin) ? [...slots, currentMin].sort((a, b) => a - b) : slots;
215
+ if (!query) {
216
+ return base;
217
+ }
218
+ const {test, exact} = matchTime(query, hourCycle);
219
+ // the typed text also matches the localized label ("p.m.", "午後")
220
+ const q = query.toLowerCase().replace(/\s+/g, "");
221
+ const hits = base.filter((t) => test(t) || label(t).toLowerCase().replace(/\s+/g, "").includes(q));
222
+ return exact !== null && !hits.includes(exact) ? [exact, ...hits] : hits;
223
+ }, [slots, currentMin, query, hourCycle, label]);
224
+
225
+ const listRef = React.useRef<HTMLDivElement>(null);
226
+
227
+ return (
228
+ <Combobox<number>
229
+ items={items}
230
+ filter={null}
231
+ // typing highlights the first match, so Enter takes it
232
+ autoHighlight
233
+ value={currentMin}
234
+ onValueChange={(next) => {
235
+ setQuery(null);
236
+ set(next === null ? null : toValue(next));
237
+ }}
238
+ inputValue={input}
239
+ onInputValueChange={(text, details) => {
240
+ setInput(text);
241
+ if (details.reason === "input-change") {
242
+ setQuery(text.trim() || null);
243
+ }
244
+ }}
245
+ onOpenChange={(open) => {
246
+ if (open) {
247
+ // the selected slot in view (Base UI highlights it; the list starts at the top)
248
+ requestAnimationFrame(() =>
249
+ listRef.current
250
+ ?.querySelector<HTMLElement>("[data-selected]")
251
+ ?.scrollIntoView({block: "center"}),
252
+ );
253
+ } else {
254
+ setQuery(null);
255
+ setInput(currentMin === null ? "" : label(currentMin));
256
+ }
257
+ }}
258
+ itemToStringLabel={label}
259
+ disabled={disabled}
260
+ name={name}
261
+ >
262
+ <ComboboxInput
263
+ id={id}
264
+ placeholder={placeholder}
265
+ disabled={disabled}
266
+ inputMode="text"
267
+ autoComplete="off"
268
+ className={cn(timeSelectVariants({size}), className)}
269
+ {...aria}
270
+ />
271
+ <ComboboxContent data-slot="time-select-content">
272
+ <ComboboxEmpty>No matching time.</ComboboxEmpty>
273
+ <ComboboxList ref={listRef}>
274
+ {(minutes: number) => (
275
+ <ComboboxItem key={minutes} value={minutes} className="tabular-nums">
276
+ <span className="font-mono">{label(minutes)}</span>
277
+ {from !== null && minutes >= from && (
278
+ <span className="ms-auto font-alt text-xs text-muted-foreground">
279
+ {formatDuration(minutes - from)}
280
+ </span>
281
+ )}
282
+ </ComboboxItem>
283
+ )}
284
+ </ComboboxList>
285
+ </ComboboxContent>
286
+ </Combobox>
287
+ );
288
+ }
289
+
290
+ export {formatDuration, formatTime, matchTime, TimeSelect, timeSelectVariants};
291
+ export type {TimeSelectProps};
@@ -0,0 +1,190 @@
1
+ "use client";
2
+
3
+ import {type Color, colorVariants} from "@repraze/kernelkit/lib/color";
4
+ import {cva} from "class-variance-authority";
5
+ import {cn} from "cn";
6
+ import * as React from "react";
7
+
8
+ type TimelineSize = "sm" | "default";
9
+
10
+ const TimelineContext = React.createContext<TimelineSize>("default");
11
+
12
+ /**
13
+ * The marker column sets the rhythm: its box is as tall as the title's line (24px, 20px at `sm`), so a dot
14
+ * or icon centres on the first line of text whatever the item holds.
15
+ */
16
+ const timelineVariants = cva("flex flex-col", {
17
+ variants: {
18
+ size: {
19
+ default: "text-sm",
20
+ sm: "text-xs",
21
+ },
22
+ },
23
+ defaultVariants: {size: "default"},
24
+ });
25
+
26
+ interface TimelineProps extends React.ComponentProps<"ol"> {
27
+ /** `sm` is the dense log: 20px rows, `text-xs`, tighter spacing. */
28
+ size?: TimelineSize;
29
+ }
30
+
31
+ /** A vertical list of events: a marker per item joined by a line, newest first or last as you order them. */
32
+ function Timeline({size = "default", className, ...props}: TimelineProps) {
33
+ return (
34
+ <TimelineContext.Provider value={size}>
35
+ <ol data-slot="timeline" data-size={size} className={cn(timelineVariants({size}), className)} {...props} />
36
+ </TimelineContext.Provider>
37
+ );
38
+ }
39
+
40
+ /** One event: `TimelineMarker` then `TimelineContent` (put a `TimelineTime` first for a time column). */
41
+ function TimelineItem({className, ...props}: React.ComponentProps<"li">) {
42
+ const size = React.useContext(TimelineContext);
43
+ return (
44
+ <li
45
+ data-slot="timeline-item"
46
+ data-size={size}
47
+ className={cn("group/timeline-item relative flex gap-3 data-[size=sm]:gap-2.5", className)}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ const timelineMarkerVariants = cva(
54
+ "relative z-10 inline-flex shrink-0 items-center justify-center rounded-pill [&_svg]:shrink-0",
55
+ {
56
+ variants: {
57
+ color: colorVariants,
58
+ kind: {
59
+ // a dot with a ring of the page colour, so it reads over the line
60
+ dot: "bg-(--c)",
61
+ icon: "border border-(--c)/40 bg-background text-(--c) hc:border-(--c)",
62
+ },
63
+ size: {
64
+ default: "",
65
+ sm: "",
66
+ },
67
+ },
68
+ compoundVariants: [
69
+ {kind: "dot", size: "default", class: "size-2.5"},
70
+ {kind: "dot", size: "sm", class: "size-2"},
71
+ {kind: "icon", size: "default", class: "size-6 [&_svg:not([class*='size-'])]:size-3.5"},
72
+ {kind: "icon", size: "sm", class: "size-5 [&_svg:not([class*='size-'])]:size-3"},
73
+ {kind: "dot", color: "neutral", class: "bg-muted-foreground"},
74
+ {kind: "icon", color: "neutral", class: "border-border text-muted-foreground hc:border-border"},
75
+ ],
76
+ defaultVariants: {color: "neutral", kind: "dot", size: "default"},
77
+ },
78
+ );
79
+
80
+ interface TimelineMarkerProps extends Omit<React.ComponentProps<"span">, "color"> {
81
+ /** Status or hue of the dot / icon ring. Defaults to neutral grey. */
82
+ color?: Color | null;
83
+ }
84
+
85
+ /**
86
+ * The item's dot, or an icon in a ring when it has children. Carries the line down to the next item;
87
+ * the last item has no tail.
88
+ */
89
+ function TimelineMarker({color, className, children, ...props}: TimelineMarkerProps) {
90
+ const size = React.useContext(TimelineContext);
91
+ const resolved = color ?? "neutral";
92
+ const kind = children ? "icon" : "dot";
93
+ return (
94
+ <span
95
+ data-slot="timeline-marker"
96
+ data-size={size}
97
+ data-color={resolved}
98
+ className="flex w-6 shrink-0 flex-col items-center self-stretch data-[size=sm]:w-5"
99
+ >
100
+ <span className="flex h-6 items-center justify-center data-[size=sm]:h-5" data-size={size}>
101
+ <span
102
+ data-slot="timeline-marker-indicator"
103
+ className={cn(timelineMarkerVariants({color: resolved, kind, size}), className)}
104
+ {...props}
105
+ >
106
+ {children}
107
+ </span>
108
+ </span>
109
+ <span
110
+ data-slot="timeline-marker-tail"
111
+ aria-hidden="true"
112
+ className={cn(
113
+ "w-px flex-1 bg-border group-last/timeline-item:hidden",
114
+ // a dot sits inside its box with room around it; an icon fills the box, so leave a gap at both ends
115
+ kind === "icon" && "my-1",
116
+ )}
117
+ />
118
+ </span>
119
+ );
120
+ }
121
+
122
+ /** Title, time and description of an item. Its bottom padding is the gap to the next item. */
123
+ function TimelineContent({className, ...props}: React.ComponentProps<"div">) {
124
+ const size = React.useContext(TimelineContext);
125
+ return (
126
+ <div
127
+ data-slot="timeline-content"
128
+ data-size={size}
129
+ className={cn(
130
+ "flex min-w-0 flex-1 flex-col gap-0.5 pb-6 group-last/timeline-item:pb-0 data-[size=sm]:gap-0 data-[size=sm]:pb-2",
131
+ className,
132
+ )}
133
+ {...props}
134
+ />
135
+ );
136
+ }
137
+
138
+ function TimelineTitle({className, ...props}: React.ComponentProps<"div">) {
139
+ const size = React.useContext(TimelineContext);
140
+ return (
141
+ <div
142
+ data-slot="timeline-title"
143
+ data-size={size}
144
+ className={cn(
145
+ "flex min-h-6 flex-wrap items-center gap-x-2 font-medium text-foreground data-[size=sm]:min-h-5 data-[size=sm]:font-normal",
146
+ className,
147
+ )}
148
+ {...props}
149
+ />
150
+ );
151
+ }
152
+
153
+ /** A timestamp: mono, muted. Inline after a title, or first in the item for a time column. */
154
+ function TimelineTime({className, ...props}: React.ComponentProps<"time">) {
155
+ const size = React.useContext(TimelineContext);
156
+ return (
157
+ <time
158
+ data-slot="timeline-time"
159
+ data-size={size}
160
+ className={cn(
161
+ "inline-flex min-h-6 shrink-0 items-center font-alt text-xs font-normal text-muted-foreground tabular-nums data-[size=sm]:min-h-5",
162
+ className,
163
+ )}
164
+ {...props}
165
+ />
166
+ );
167
+ }
168
+
169
+ function TimelineDescription({className, ...props}: React.ComponentProps<"div">) {
170
+ return (
171
+ <div
172
+ data-slot="timeline-description"
173
+ className={cn("text-sm text-muted-foreground in-data-[size=sm]:text-xs", className)}
174
+ {...props}
175
+ />
176
+ );
177
+ }
178
+
179
+ export {
180
+ Timeline,
181
+ TimelineContent,
182
+ TimelineDescription,
183
+ TimelineItem,
184
+ TimelineMarker,
185
+ timelineMarkerVariants,
186
+ TimelineTime,
187
+ TimelineTitle,
188
+ timelineVariants,
189
+ };
190
+ export type {TimelineMarkerProps, TimelineProps, TimelineSize};
@@ -2,10 +2,11 @@
2
2
 
3
3
  import {Toast as ToastPrimitive} from "@base-ui/react/toast";
4
4
  import {Button} from "@repraze/kernelkit/components/button";
5
+ import {Spinner} from "@repraze/kernelkit/components/spinner";
5
6
  import {focusRing, heading} from "@repraze/kernelkit/lib/recipes";
6
7
  import {usePortalContainer} from "@repraze/kernelkit/provider";
7
8
  import {cn} from "cn";
8
- import {CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon} from "lucide-react";
9
+ import {CircleCheckIcon, InfoIcon, OctagonXIcon, TriangleAlertIcon, XIcon} from "lucide-react";
9
10
  import * as React from "react";
10
11
 
11
12
  const toast = ToastPrimitive.createToastManager();
@@ -136,9 +137,8 @@ const TOAST_ICONS: Record<string, React.ReactNode> = {
136
137
  warning: <TriangleAlertIcon className="text-warning" aria-hidden="true" />,
137
138
  error: <OctagonXIcon className="text-danger" aria-hidden="true" />,
138
139
  danger: <OctagonXIcon className="text-danger" aria-hidden="true" />,
139
- loading: (
140
- <Loader2Icon className="animate-spin text-muted-foreground motion-reduce:animate-none" aria-hidden="true" />
141
- ),
140
+ // follows the app's SpinnerProvider like every other kit spinner
141
+ loading: <Spinner className="text-muted-foreground" role="presentation" aria-label={undefined} />,
142
142
  };
143
143
 
144
144
  function ToastIcon({type, className}: {type: string | undefined; className?: string}) {