@repraze/kernelkit 0.1.0-alpha.3 → 0.1.0-alpha.5
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/README.md +17 -0
- package/dist/components/alert-dialog.d.ts.map +1 -1
- package/dist/components/alert-dialog.js +4 -1
- package/dist/components/alert-dialog.js.map +1 -1
- package/dist/components/alert.js +3 -3
- package/dist/components/alert.js.map +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/badge.js.map +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar.d.ts +8 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +33 -11
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/callout.js +2 -2
- package/dist/components/callout.js.map +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/date-picker.d.ts +11 -5
- package/dist/components/date-picker.d.ts.map +1 -1
- package/dist/components/date-picker.js +29 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +26 -0
- package/dist/components/description-list.d.ts.map +1 -0
- package/dist/components/description-list.js +59 -0
- package/dist/components/description-list.js.map +1 -0
- package/dist/components/locale.d.ts +19 -0
- package/dist/components/locale.d.ts.map +1 -0
- package/dist/components/locale.js +54 -0
- package/dist/components/locale.js.map +1 -0
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/popover.js +2 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress.d.ts +19 -6
- package/dist/components/progress.d.ts.map +1 -1
- package/dist/components/progress.js +18 -3
- package/dist/components/progress.js.map +1 -1
- package/dist/components/search-dialog.d.ts +6 -1
- package/dist/components/search-dialog.d.ts.map +1 -1
- package/dist/components/search-dialog.js +13 -1
- package/dist/components/search-dialog.js.map +1 -1
- package/dist/components/search-trigger.d.ts +4 -1
- package/dist/components/search-trigger.d.ts.map +1 -1
- package/dist/components/search-trigger.js +5 -2
- package/dist/components/search-trigger.js.map +1 -1
- package/dist/components/shimmer.d.ts +13 -0
- package/dist/components/shimmer.d.ts.map +1 -0
- package/dist/components/shimmer.js +21 -0
- package/dist/components/shimmer.js.map +1 -0
- package/dist/components/spinner.d.ts +28 -6
- package/dist/components/spinner.d.ts.map +1 -1
- package/dist/components/spinner.js +66 -14
- package/dist/components/spinner.js.map +1 -1
- package/dist/components/stepper.d.ts +64 -0
- package/dist/components/stepper.d.ts.map +1 -0
- package/dist/components/stepper.js +167 -0
- package/dist/components/stepper.js.map +1 -0
- package/dist/components/tag-input.d.ts +47 -0
- package/dist/components/tag-input.d.ts.map +1 -0
- package/dist/components/tag-input.js +211 -0
- package/dist/components/tag-input.js.map +1 -0
- package/dist/components/time-input.d.ts +75 -0
- package/dist/components/time-input.d.ts.map +1 -0
- package/dist/components/time-input.js +341 -0
- package/dist/components/time-input.js.map +1 -0
- package/dist/components/time-select.d.ts +49 -0
- package/dist/components/time-select.d.ts.map +1 -0
- package/dist/components/time-select.js +178 -0
- package/dist/components/time-select.js.map +1 -0
- package/dist/components/timeline.d.ts +41 -0
- package/dist/components/timeline.d.ts.map +1 -0
- package/dist/components/timeline.js +83 -0
- package/dist/components/timeline.js.map +1 -0
- package/dist/components/toast.d.ts.map +1 -1
- package/dist/components/toast.js +4 -2
- package/dist/components/toast.js.map +1 -1
- package/dist/components/tree.d.ts +87 -0
- package/dist/components/tree.d.ts.map +1 -0
- package/dist/components/tree.js +516 -0
- package/dist/components/tree.js.map +1 -0
- package/dist/docs/doc-kit.d.ts +72 -0
- package/dist/docs/doc-kit.d.ts.map +1 -0
- package/dist/docs/doc-kit.js +45 -0
- package/dist/docs/doc-kit.js.map +1 -0
- package/dist/docs/doc-page.d.ts +27 -0
- package/dist/docs/doc-page.d.ts.map +1 -0
- package/dist/docs/doc-page.js +33 -0
- package/dist/docs/doc-page.js.map +1 -0
- package/dist/docs/docs-index.d.ts +60 -0
- package/dist/docs/docs-index.d.ts.map +1 -0
- package/dist/docs/docs-index.js +45 -0
- package/dist/docs/docs-index.js.map +1 -0
- package/dist/docs/docs-nav.d.ts +13 -0
- package/dist/docs/docs-nav.d.ts.map +1 -0
- package/dist/docs/docs-nav.js +8 -0
- package/dist/docs/docs-nav.js.map +1 -0
- package/dist/docs/docs-search.d.ts +19 -0
- package/dist/docs/docs-search.d.ts.map +1 -0
- package/dist/docs/docs-search.js +33 -0
- package/dist/docs/docs-search.js.map +1 -0
- package/dist/docs/index.d.ts +7 -0
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +7 -0
- package/dist/docs/index.js.map +1 -0
- package/dist/docs/review-matrix.d.ts +25 -0
- package/dist/docs/review-matrix.d.ts.map +1 -0
- package/dist/docs/review-matrix.js +31 -0
- package/dist/docs/review-matrix.js.map +1 -0
- package/dist/hooks/use-theme-palette.d.ts +8 -0
- package/dist/hooks/use-theme-palette.d.ts.map +1 -0
- package/dist/hooks/use-theme-palette.js +24 -0
- package/dist/hooks/use-theme-palette.js.map +1 -0
- package/dist/hooks/use-time-format.d.ts +23 -0
- package/dist/hooks/use-time-format.d.ts.map +1 -0
- package/dist/hooks/use-time-format.js +52 -0
- package/dist/hooks/use-time-format.js.map +1 -0
- package/dist/lib/color.d.ts +5 -1
- package/dist/lib/color.d.ts.map +1 -1
- package/dist/lib/color.js +14 -2
- package/dist/lib/color.js.map +1 -1
- package/dist/lib/recipes.d.ts +5 -4
- package/dist/lib/recipes.d.ts.map +1 -1
- package/dist/lib/recipes.js +7 -5
- package/dist/lib/recipes.js.map +1 -1
- package/dist/provider/theme-provider.d.ts +10 -1
- package/dist/provider/theme-provider.d.ts.map +1 -1
- package/dist/provider/theme-provider.js +23 -8
- package/dist/provider/theme-provider.js.map +1 -1
- package/dist/site/brand.d.ts +20 -0
- package/dist/site/brand.d.ts.map +1 -0
- package/dist/site/brand.js +17 -0
- package/dist/site/brand.js.map +1 -0
- package/dist/site/github-icon.d.ts +4 -0
- package/dist/site/github-icon.d.ts.map +1 -0
- package/dist/site/github-icon.js +6 -0
- package/dist/site/github-icon.js.map +1 -0
- package/dist/site/index.d.ts +6 -0
- package/dist/site/index.d.ts.map +1 -0
- package/dist/site/index.js +6 -0
- package/dist/site/index.js.map +1 -0
- package/dist/site/suite-footer.d.ts +24 -0
- package/dist/site/suite-footer.d.ts.map +1 -0
- package/dist/site/suite-footer.js +19 -0
- package/dist/site/suite-footer.js.map +1 -0
- package/dist/site/suite-header.d.ts +40 -0
- package/dist/site/suite-header.d.ts.map +1 -0
- package/dist/site/suite-header.js +19 -0
- package/dist/site/suite-header.js.map +1 -0
- package/dist/site/use-page-title.d.ts +3 -0
- package/dist/site/use-page-title.d.ts.map +1 -0
- package/dist/site/use-page-title.js +8 -0
- package/dist/site/use-page-title.js.map +1 -0
- package/dist/vite/index.d.ts +65 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +242 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +39 -3
- package/src/components/alert-dialog.tsx +4 -1
- package/src/components/alert.tsx +3 -3
- package/src/components/badge.tsx +1 -1
- package/src/components/button.tsx +1 -1
- package/src/components/calendar.tsx +48 -4
- package/src/components/callout.tsx +2 -2
- package/src/components/combobox.tsx +1 -1
- package/src/components/date-picker.tsx +43 -9
- package/src/components/description-list.tsx +107 -0
- package/src/components/locale.tsx +67 -0
- package/src/components/popover.tsx +3 -1
- package/src/components/progress.tsx +70 -4
- package/src/components/search-dialog.tsx +14 -1
- package/src/components/search-trigger.tsx +8 -2
- package/src/components/shimmer.tsx +36 -0
- package/src/components/spinner.tsx +126 -23
- package/src/components/stepper.tsx +346 -0
- package/src/components/tag-input.tsx +345 -0
- package/src/components/time-input.tsx +578 -0
- package/src/components/time-select.tsx +291 -0
- package/src/components/timeline.tsx +190 -0
- package/src/components/toast.tsx +4 -4
- package/src/components/tree.tsx +832 -0
- package/src/docs/doc-kit.tsx +176 -0
- package/src/docs/doc-page.tsx +126 -0
- package/src/docs/docs-index.ts +108 -0
- package/src/docs/docs-nav.tsx +47 -0
- package/src/docs/docs-search.tsx +88 -0
- package/src/docs/index.ts +6 -0
- package/src/docs/review-matrix.tsx +67 -0
- package/src/hooks/use-theme-palette.ts +25 -0
- package/src/hooks/use-time-format.ts +72 -0
- package/src/lib/color.ts +19 -2
- package/src/lib/recipes.ts +7 -5
- package/src/provider/theme-provider.tsx +34 -9
- package/src/site/brand.tsx +41 -0
- package/src/site/github-icon.tsx +10 -0
- package/src/site/index.ts +5 -0
- package/src/site/suite-footer.tsx +68 -0
- package/src/site/suite-header.tsx +114 -0
- package/src/site/use-page-title.ts +8 -0
- package/src/styles/kernelkit.css +58 -0
- package/src/styles/site.css +15 -0
- package/src/vite/client.d.ts +11 -0
- package/src/vite/index.ts +298 -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};
|
package/src/components/toast.tsx
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
140
|
-
|
|
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}) {
|