@momentumworld/ui-react 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2HQPCV2L.js +895 -0
- package/dist/chunk-2HQPCV2L.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +356 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +2162 -0
- package/dist/index.js +11640 -0
- package/dist/index.js.map +1 -0
- package/dist/input-JCZs61MO.d.ts +24 -0
- package/package.json +90 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/layout/dashboard-topbar.tsx +221 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-counter.tsx +109 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/aurora-orb.tsx +163 -0
- package/src/components/ui/aurora-text.tsx +76 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +150 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/chat-divider.tsx +67 -0
- package/src/components/ui/chat.tsx +494 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/color-picker.tsx +389 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/confetti.tsx +196 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/context-meter.tsx +134 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/data-table-paged.tsx +163 -0
- package/src/components/ui/date-picker.tsx +154 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/fab.tsx +45 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/help-tip.tsx +94 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/markdown.tsx +188 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/pipeline.tsx +102 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/segmented-control.tsx +83 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +774 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +206 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +242 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/thinking-orb.tsx +6 -0
- package/src/components/ui/time-picker.tsx +308 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +143 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +329 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { format } from "date-fns";
|
|
4
|
+
import { CalendarClockIcon, ClockIcon } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { Button } from "./button";
|
|
8
|
+
import { Calendar } from "./calendar";
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
10
|
+
import { cn } from "../../lib/utils/css";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A time of day is a STRING here, not a `Date`.
|
|
14
|
+
*
|
|
15
|
+
* "Nine in the morning, every Tuesday" has no date, and giving it one forces every
|
|
16
|
+
* caller to invent a day and then remember to ignore it — which is how a weekly
|
|
17
|
+
* opening hour ends up shifted by a timezone that was never part of the question.
|
|
18
|
+
* `TimePicker` therefore speaks `"HH:mm"`, zero-padded and 24-hour, which is also
|
|
19
|
+
* what a DRF `TimeField` parses. `DateTimePicker` below takes a real `Date`, because
|
|
20
|
+
* an instant genuinely is one.
|
|
21
|
+
*/
|
|
22
|
+
type TimeValue = string;
|
|
23
|
+
|
|
24
|
+
/** `"09:30"` → 570. Anything unparseable is `null` — read as "no time yet". */
|
|
25
|
+
function toMinutes(value?: TimeValue | null): number | null {
|
|
26
|
+
if (!value) return null;
|
|
27
|
+
const match = /^(\d{1,2}):(\d{2})/.exec(value.trim());
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
const hours = Number(match[1]);
|
|
30
|
+
const minutes = Number(match[2]);
|
|
31
|
+
if (hours > 23 || minutes > 59) return null;
|
|
32
|
+
return hours * 60 + minutes;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 570 → `"09:30"`. The value on the wire. */
|
|
36
|
+
function toValue(minutes: number): TimeValue {
|
|
37
|
+
return `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 570 → `"9:30 AM"`, or `"09:30"` on a 24-hour clock. What a person reads. */
|
|
41
|
+
function toLabel(minutes: number, hour12: boolean): string {
|
|
42
|
+
if (!hour12) return toValue(minutes);
|
|
43
|
+
const hours = Math.floor(minutes / 60);
|
|
44
|
+
const twelve = hours % 12 === 0 ? 12 : hours % 12;
|
|
45
|
+
return `${twelve}:${String(minutes % 60).padStart(2, "0")} ${hours < 12 ? "AM" : "PM"}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Every offered time, as minutes past midnight. */
|
|
49
|
+
function timeOptions(step: number, min?: TimeValue, max?: TimeValue): number[] {
|
|
50
|
+
const size = Math.max(1, Math.round(step));
|
|
51
|
+
const first = toMinutes(min) ?? 0;
|
|
52
|
+
const last = toMinutes(max) ?? 24 * 60 - 1;
|
|
53
|
+
const out: number[] = [];
|
|
54
|
+
for (let at = Math.ceil(first / size) * size; at <= last; at += size) out.push(at);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The list of times inside a popover.
|
|
60
|
+
*
|
|
61
|
+
* A plain `overflow-y-auto` rather than `ScrollArea`: this list opens scrolled to the
|
|
62
|
+
* current value, and setting `scrollTop` on an element we own is exact. The obvious
|
|
63
|
+
* alternative, `scrollIntoView`, walks up the tree and will scroll the dialog behind
|
|
64
|
+
* the popover along with the list inside it.
|
|
65
|
+
*/
|
|
66
|
+
function TimeList({
|
|
67
|
+
hour12,
|
|
68
|
+
onSelect,
|
|
69
|
+
options,
|
|
70
|
+
selected,
|
|
71
|
+
}: {
|
|
72
|
+
hour12: boolean;
|
|
73
|
+
onSelect: (minutes: number) => void;
|
|
74
|
+
options: number[];
|
|
75
|
+
selected: number | null;
|
|
76
|
+
}) {
|
|
77
|
+
const listRef = React.useRef<HTMLDivElement>(null);
|
|
78
|
+
const activeRef = React.useRef<HTMLButtonElement>(null);
|
|
79
|
+
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
const list = listRef.current;
|
|
82
|
+
const active = activeRef.current;
|
|
83
|
+
if (!list || !active) return;
|
|
84
|
+
list.scrollTop = active.offsetTop - list.clientHeight / 2 + active.clientHeight / 2;
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
className="max-h-64 w-32 shrink-0 overflow-y-auto p-1"
|
|
90
|
+
data-slot="time-picker-list"
|
|
91
|
+
ref={listRef}
|
|
92
|
+
role="listbox"
|
|
93
|
+
>
|
|
94
|
+
{options.map((minutes) => {
|
|
95
|
+
const isSelected = minutes === selected;
|
|
96
|
+
return (
|
|
97
|
+
<button
|
|
98
|
+
aria-selected={isSelected}
|
|
99
|
+
className={cn(
|
|
100
|
+
"flex w-full items-center rounded-lg px-2.5 py-1.5 text-left text-base sm:text-sm text-foreground outline-none",
|
|
101
|
+
"hover:bg-accent focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
102
|
+
isSelected && "bg-primary text-primary-foreground hover:bg-primary",
|
|
103
|
+
)}
|
|
104
|
+
key={minutes}
|
|
105
|
+
onClick={() => onSelect(minutes)}
|
|
106
|
+
ref={isSelected ? activeRef : undefined}
|
|
107
|
+
role="option"
|
|
108
|
+
type="button"
|
|
109
|
+
>
|
|
110
|
+
{toLabel(minutes, hour12)}
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
{options.length === 0 ? (
|
|
115
|
+
<p className="px-2.5 py-1.5 text-sm text-muted-foreground">No time in range.</p>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface TimePickerProps {
|
|
122
|
+
/** The selected time as `"HH:mm"` (controlled). */
|
|
123
|
+
value?: TimeValue | null;
|
|
124
|
+
onValueChange?: (value: TimeValue | undefined) => void;
|
|
125
|
+
placeholder?: string;
|
|
126
|
+
disabled?: boolean;
|
|
127
|
+
/** Minutes between offered times. 15 by default; 5 and 30 are the other useful ones. */
|
|
128
|
+
step?: number;
|
|
129
|
+
/** Bounds, inclusive, as `"HH:mm"`. */
|
|
130
|
+
min?: TimeValue;
|
|
131
|
+
max?: TimeValue;
|
|
132
|
+
/** `false` shows a 24-hour clock. The value on the wire is 24-hour either way. */
|
|
133
|
+
hour12?: boolean;
|
|
134
|
+
align?: "start" | "center" | "end";
|
|
135
|
+
className?: string;
|
|
136
|
+
id?: string;
|
|
137
|
+
"aria-label"?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A time picker: a button that opens a list of times in a popover. The canonical way
|
|
142
|
+
* to take a time of day in this system — never a native `<input type="time">`, which
|
|
143
|
+
* draws the *browser's* control (light in dark mode, its own glyph, its own idea of a
|
|
144
|
+
* locale) in the middle of one of our forms, and asks somebody to type `--:-- --`.
|
|
145
|
+
*
|
|
146
|
+
* Deliberately a fixed list rather than two spinners: the times these are used for are
|
|
147
|
+
* office hours and appointment slots, which land on the quarter hour, and a list can be
|
|
148
|
+
* read and clicked in one gesture. Pass `step={5}` where a finer time is real.
|
|
149
|
+
*/
|
|
150
|
+
function TimePicker({
|
|
151
|
+
value,
|
|
152
|
+
onValueChange,
|
|
153
|
+
placeholder = "Pick a time",
|
|
154
|
+
disabled,
|
|
155
|
+
step = 15,
|
|
156
|
+
min,
|
|
157
|
+
max,
|
|
158
|
+
hour12 = true,
|
|
159
|
+
align = "start",
|
|
160
|
+
className,
|
|
161
|
+
id,
|
|
162
|
+
...props
|
|
163
|
+
}: TimePickerProps) {
|
|
164
|
+
const [open, setOpen] = React.useState(false);
|
|
165
|
+
const selected = toMinutes(value);
|
|
166
|
+
const options = React.useMemo(() => timeOptions(step, min, max), [step, min, max]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Popover onOpenChange={setOpen} open={open}>
|
|
170
|
+
<PopoverTrigger
|
|
171
|
+
render={
|
|
172
|
+
<Button
|
|
173
|
+
aria-label={props["aria-label"]}
|
|
174
|
+
className={cn("w-full justify-start font-normal", selected === null && "text-muted-foreground", className)}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
id={id}
|
|
177
|
+
variant="outline"
|
|
178
|
+
/>
|
|
179
|
+
}
|
|
180
|
+
>
|
|
181
|
+
<ClockIcon aria-hidden />
|
|
182
|
+
{selected === null ? <span>{placeholder}</span> : toLabel(selected, hour12)}
|
|
183
|
+
</PopoverTrigger>
|
|
184
|
+
<PopoverContent align={align} className="w-auto p-0" data-slot="time-picker">
|
|
185
|
+
{open ? (
|
|
186
|
+
<TimeList
|
|
187
|
+
hour12={hour12}
|
|
188
|
+
onSelect={(minutes) => {
|
|
189
|
+
onValueChange?.(toValue(minutes));
|
|
190
|
+
setOpen(false);
|
|
191
|
+
}}
|
|
192
|
+
options={options}
|
|
193
|
+
selected={selected}
|
|
194
|
+
/>
|
|
195
|
+
) : null}
|
|
196
|
+
</PopoverContent>
|
|
197
|
+
</Popover>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface DateTimePickerProps {
|
|
202
|
+
/** The selected instant (controlled). */
|
|
203
|
+
value?: Date;
|
|
204
|
+
onValueChange?: (date: Date | undefined) => void;
|
|
205
|
+
placeholder?: string;
|
|
206
|
+
disabled?: boolean;
|
|
207
|
+
fromDate?: Date;
|
|
208
|
+
toDate?: Date;
|
|
209
|
+
step?: number;
|
|
210
|
+
hour12?: boolean;
|
|
211
|
+
/** The time a day picked before any time gets. */
|
|
212
|
+
defaultTime?: TimeValue;
|
|
213
|
+
/** date-fns format for the date half of the trigger label. */
|
|
214
|
+
displayFormat?: string;
|
|
215
|
+
align?: "start" | "center" | "end";
|
|
216
|
+
className?: string;
|
|
217
|
+
id?: string;
|
|
218
|
+
"aria-label"?: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A date *and* a time, in one popover: the calendar beside the list of times.
|
|
223
|
+
*
|
|
224
|
+
* One control and not two fields, because the two halves are one answer — "from when"
|
|
225
|
+
* — and two controls make it possible to submit half of it. Picking a day keeps the
|
|
226
|
+
* time already chosen (or `defaultTime`), and picking a time keeps the day, so the
|
|
227
|
+
* order they are pressed in does not matter.
|
|
228
|
+
*/
|
|
229
|
+
function DateTimePicker({
|
|
230
|
+
value,
|
|
231
|
+
onValueChange,
|
|
232
|
+
placeholder = "Pick a date and time",
|
|
233
|
+
disabled,
|
|
234
|
+
fromDate,
|
|
235
|
+
toDate,
|
|
236
|
+
step = 15,
|
|
237
|
+
hour12 = true,
|
|
238
|
+
defaultTime = "09:00",
|
|
239
|
+
displayFormat = "LLL dd, y",
|
|
240
|
+
align = "start",
|
|
241
|
+
className,
|
|
242
|
+
id,
|
|
243
|
+
...props
|
|
244
|
+
}: DateTimePickerProps) {
|
|
245
|
+
const [open, setOpen] = React.useState(false);
|
|
246
|
+
const options = React.useMemo(() => timeOptions(step), [step]);
|
|
247
|
+
const selectedMinutes = value ? value.getHours() * 60 + value.getMinutes() : null;
|
|
248
|
+
|
|
249
|
+
/** A new instant from a day and a time, without ever going through UTC. */
|
|
250
|
+
const combine = (day: Date, minutes: number): Date => {
|
|
251
|
+
const next = new Date(day);
|
|
252
|
+
next.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
|
|
253
|
+
return next;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<Popover onOpenChange={setOpen} open={open}>
|
|
258
|
+
<PopoverTrigger
|
|
259
|
+
render={
|
|
260
|
+
<Button
|
|
261
|
+
aria-label={props["aria-label"]}
|
|
262
|
+
className={cn("w-full justify-start font-normal", !value && "text-muted-foreground", className)}
|
|
263
|
+
disabled={disabled}
|
|
264
|
+
id={id}
|
|
265
|
+
variant="outline"
|
|
266
|
+
/>
|
|
267
|
+
}
|
|
268
|
+
>
|
|
269
|
+
<CalendarClockIcon aria-hidden />
|
|
270
|
+
{value && selectedMinutes !== null ? (
|
|
271
|
+
`${format(value, displayFormat)} · ${toLabel(selectedMinutes, hour12)}`
|
|
272
|
+
) : (
|
|
273
|
+
<span>{placeholder}</span>
|
|
274
|
+
)}
|
|
275
|
+
</PopoverTrigger>
|
|
276
|
+
<PopoverContent align={align} className="w-auto p-0" data-slot="date-time-picker">
|
|
277
|
+
<div className="flex divide-x divide-border">
|
|
278
|
+
<Calendar
|
|
279
|
+
autoFocus
|
|
280
|
+
disabled={
|
|
281
|
+
fromDate || toDate
|
|
282
|
+
? [...(fromDate ? [{ before: fromDate }] : []), ...(toDate ? [{ after: toDate }] : [])]
|
|
283
|
+
: undefined
|
|
284
|
+
}
|
|
285
|
+
mode="single"
|
|
286
|
+
onSelect={(day) =>
|
|
287
|
+
onValueChange?.(day ? combine(day, selectedMinutes ?? toMinutes(defaultTime) ?? 0) : undefined)
|
|
288
|
+
}
|
|
289
|
+
selected={value}
|
|
290
|
+
/>
|
|
291
|
+
{open ? (
|
|
292
|
+
<TimeList
|
|
293
|
+
hour12={hour12}
|
|
294
|
+
onSelect={(minutes) => {
|
|
295
|
+
onValueChange?.(combine(value ?? new Date(), minutes));
|
|
296
|
+
setOpen(false);
|
|
297
|
+
}}
|
|
298
|
+
options={options}
|
|
299
|
+
selected={selectedMinutes}
|
|
300
|
+
/>
|
|
301
|
+
) : null}
|
|
302
|
+
</div>
|
|
303
|
+
</PopoverContent>
|
|
304
|
+
</Popover>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export { DateTimePicker, TimePicker, type DateTimePickerProps, type TimePickerProps, type TimeValue };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils/css";
|
|
6
|
+
|
|
7
|
+
function Timeline({ className, ...props }: React.ComponentProps<"ol">) {
|
|
8
|
+
return (
|
|
9
|
+
<ol
|
|
10
|
+
className={cn(
|
|
11
|
+
"flex flex-col [&>li:last-child_[data-slot=timeline-connector]]:hidden",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
data-slot="timeline"
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TimelineItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
21
|
+
return (
|
|
22
|
+
<li
|
|
23
|
+
className={cn("group/timeline-item flex gap-4", className)}
|
|
24
|
+
data-slot="timeline-item"
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The marker column: a dot and the connecting line beneath it. */
|
|
31
|
+
function TimelineDot({
|
|
32
|
+
className,
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<"div">) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className="flex w-7 shrink-0 flex-col items-center"
|
|
39
|
+
data-slot="timeline-marker"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
"mt-0.5 flex size-3 shrink-0 items-center justify-center rounded-full border-2 border-primary bg-background [&_svg]:size-3.5 has-[svg]:mt-0 has-[svg]:size-7 has-[svg]:border-transparent has-[svg]:bg-primary has-[svg]:text-primary-foreground",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
data-slot="timeline-dot"
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
<div
|
|
52
|
+
className="my-1 w-0.5 flex-1 rounded-full bg-border"
|
|
53
|
+
data-slot="timeline-connector"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function TimelineContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
"flex flex-1 flex-col gap-1 pb-8 group-last/timeline-item:pb-0",
|
|
64
|
+
className,
|
|
65
|
+
)}
|
|
66
|
+
data-slot="timeline-content"
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function TimelineTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={cn("font-medium text-sm leading-tight", className)}
|
|
76
|
+
data-slot="timeline-title"
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function TimelineTime({ className, ...props }: React.ComponentProps<"div">) {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={cn(
|
|
86
|
+
"font-medium text-muted-foreground text-xs tabular-nums",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
data-slot="timeline-time"
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function TimelineDescription({
|
|
96
|
+
className,
|
|
97
|
+
...props
|
|
98
|
+
}: React.ComponentProps<"div">) {
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
102
|
+
data-slot="timeline-description"
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
Timeline,
|
|
110
|
+
TimelineItem,
|
|
111
|
+
TimelineDot,
|
|
112
|
+
TimelineContent,
|
|
113
|
+
TimelineTitle,
|
|
114
|
+
TimelineTime,
|
|
115
|
+
TimelineDescription,
|
|
116
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Toast } from "@base-ui/react/toast";
|
|
4
|
+
import {
|
|
5
|
+
CircleAlertIcon,
|
|
6
|
+
CircleCheckIcon,
|
|
7
|
+
InfoIcon,
|
|
8
|
+
LoaderCircleIcon,
|
|
9
|
+
TriangleAlertIcon,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { buttonVariants } from "./button";
|
|
12
|
+
import { cn } from "../../lib/utils/css";
|
|
13
|
+
|
|
14
|
+
const toastManager = Toast.createToastManager();
|
|
15
|
+
const anchoredToastManager = Toast.createToastManager();
|
|
16
|
+
|
|
17
|
+
const TOAST_ICONS = {
|
|
18
|
+
error: CircleAlertIcon,
|
|
19
|
+
info: InfoIcon,
|
|
20
|
+
loading: LoaderCircleIcon,
|
|
21
|
+
success: CircleCheckIcon,
|
|
22
|
+
warning: TriangleAlertIcon,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
type ToastPosition =
|
|
26
|
+
| "top-left"
|
|
27
|
+
| "top-center"
|
|
28
|
+
| "top-right"
|
|
29
|
+
| "bottom-left"
|
|
30
|
+
| "bottom-center"
|
|
31
|
+
| "bottom-right";
|
|
32
|
+
|
|
33
|
+
interface ToastProviderProps extends Toast.Provider.Props {
|
|
34
|
+
position?: ToastPosition;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ToastProvider({
|
|
38
|
+
children,
|
|
39
|
+
position = "bottom-right",
|
|
40
|
+
...props
|
|
41
|
+
}: ToastProviderProps) {
|
|
42
|
+
return (
|
|
43
|
+
<Toast.Provider toastManager={toastManager} {...props}>
|
|
44
|
+
{children}
|
|
45
|
+
<Toasts position={position} />
|
|
46
|
+
</Toast.Provider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Toasts({ position = "bottom-right" }: { position: ToastPosition }) {
|
|
51
|
+
const { toasts } = Toast.useToastManager();
|
|
52
|
+
const isTop = position.startsWith("top");
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Toast.Portal data-slot="toast-portal">
|
|
56
|
+
<Toast.Viewport
|
|
57
|
+
className={cn(
|
|
58
|
+
"fixed z-60 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]",
|
|
59
|
+
// Vertical positioning
|
|
60
|
+
"data-[position*=top]:top-(--toast-inset)",
|
|
61
|
+
"data-[position*=bottom]:bottom-(--toast-inset)",
|
|
62
|
+
// Horizontal positioning
|
|
63
|
+
"data-[position*=left]:left-(--toast-inset)",
|
|
64
|
+
"data-[position*=right]:right-(--toast-inset)",
|
|
65
|
+
"data-[position*=center]:-translate-x-1/2 data-[position*=center]:left-1/2",
|
|
66
|
+
)}
|
|
67
|
+
data-position={position}
|
|
68
|
+
data-slot="toast-viewport"
|
|
69
|
+
>
|
|
70
|
+
{toasts.map((toast) => {
|
|
71
|
+
const Icon = toast.type
|
|
72
|
+
? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
|
|
73
|
+
: null;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Toast.Root
|
|
77
|
+
className={cn(
|
|
78
|
+
"absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
|
|
79
|
+
// Base positioning using data-position
|
|
80
|
+
"data-[position*=right]:right-0 data-[position*=right]:left-auto",
|
|
81
|
+
"data-[position*=left]:right-auto data-[position*=left]:left-0",
|
|
82
|
+
"data-[position*=center]:right-0 data-[position*=center]:left-0",
|
|
83
|
+
"data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
|
|
84
|
+
"data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
|
|
85
|
+
// Gap fill for hover
|
|
86
|
+
"after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
|
|
87
|
+
"data-[position*=top]:after:top-full",
|
|
88
|
+
"data-[position*=bottom]:after:bottom-full",
|
|
89
|
+
// Define some variables
|
|
90
|
+
"[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
|
|
91
|
+
// Define offset-y variable
|
|
92
|
+
"data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
|
|
93
|
+
"data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
|
|
94
|
+
// Default state transform
|
|
95
|
+
"data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
|
|
96
|
+
"data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
|
|
97
|
+
// Limited state
|
|
98
|
+
"data-limited:opacity-0",
|
|
99
|
+
// Expanded state
|
|
100
|
+
"data-expanded:h-(--toast-height)",
|
|
101
|
+
"data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
|
|
102
|
+
// Starting and ending animations
|
|
103
|
+
"data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
|
|
104
|
+
"data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
|
|
105
|
+
"data-ending-style:opacity-0",
|
|
106
|
+
// Ending animations (direction-aware)
|
|
107
|
+
"data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]",
|
|
108
|
+
"data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
|
|
109
|
+
"data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
|
|
110
|
+
"data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
|
|
111
|
+
"data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
|
|
112
|
+
// Ending animations (expanded)
|
|
113
|
+
"data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
|
|
114
|
+
"data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
|
|
115
|
+
"data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
|
|
116
|
+
"data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
|
|
117
|
+
)}
|
|
118
|
+
data-position={position}
|
|
119
|
+
key={toast.id}
|
|
120
|
+
swipeDirection={
|
|
121
|
+
position.includes("center")
|
|
122
|
+
? [isTop ? "up" : "down"]
|
|
123
|
+
: position.includes("left")
|
|
124
|
+
? ["left", isTop ? "up" : "down"]
|
|
125
|
+
: ["right", isTop ? "up" : "down"]
|
|
126
|
+
}
|
|
127
|
+
toast={toast}
|
|
128
|
+
>
|
|
129
|
+
<Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100">
|
|
130
|
+
<div className="flex gap-2">
|
|
131
|
+
{Icon && (
|
|
132
|
+
<div
|
|
133
|
+
className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
|
134
|
+
data-slot="toast-icon"
|
|
135
|
+
>
|
|
136
|
+
<Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
<div className="flex flex-col gap-0.5">
|
|
141
|
+
<Toast.Title
|
|
142
|
+
className="font-medium"
|
|
143
|
+
data-slot="toast-title"
|
|
144
|
+
/>
|
|
145
|
+
<Toast.Description
|
|
146
|
+
className="text-muted-foreground"
|
|
147
|
+
data-slot="toast-description"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
{toast.actionProps && (
|
|
152
|
+
<Toast.Action
|
|
153
|
+
className={buttonVariants({ size: "xs" })}
|
|
154
|
+
data-slot="toast-action"
|
|
155
|
+
>
|
|
156
|
+
{toast.actionProps.children}
|
|
157
|
+
</Toast.Action>
|
|
158
|
+
)}
|
|
159
|
+
</Toast.Content>
|
|
160
|
+
</Toast.Root>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</Toast.Viewport>
|
|
164
|
+
</Toast.Portal>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
|
|
169
|
+
return (
|
|
170
|
+
<Toast.Provider toastManager={anchoredToastManager} {...props}>
|
|
171
|
+
{children}
|
|
172
|
+
<AnchoredToasts />
|
|
173
|
+
</Toast.Provider>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function AnchoredToasts() {
|
|
178
|
+
const { toasts } = Toast.useToastManager();
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Toast.Portal data-slot="toast-portal-anchored">
|
|
182
|
+
<Toast.Viewport
|
|
183
|
+
className="outline-none"
|
|
184
|
+
data-slot="toast-viewport-anchored"
|
|
185
|
+
>
|
|
186
|
+
{toasts.map((toast) => {
|
|
187
|
+
const Icon = toast.type
|
|
188
|
+
? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
|
|
189
|
+
: null;
|
|
190
|
+
const tooltipStyle =
|
|
191
|
+
(toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
|
|
192
|
+
const positionerProps = toast.positionerProps;
|
|
193
|
+
|
|
194
|
+
if (!positionerProps?.anchor) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<Toast.Positioner
|
|
200
|
+
className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
|
|
201
|
+
data-slot="toast-positioner"
|
|
202
|
+
key={toast.id}
|
|
203
|
+
sideOffset={positionerProps.sideOffset ?? 4}
|
|
204
|
+
toast={toast}
|
|
205
|
+
>
|
|
206
|
+
<Toast.Root
|
|
207
|
+
className={cn(
|
|
208
|
+
"relative text-balance border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
|
|
209
|
+
tooltipStyle
|
|
210
|
+
? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]"
|
|
211
|
+
: "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]",
|
|
212
|
+
)}
|
|
213
|
+
data-slot="toast-popup"
|
|
214
|
+
toast={toast}
|
|
215
|
+
>
|
|
216
|
+
{tooltipStyle ? (
|
|
217
|
+
<Toast.Content className="pointer-events-auto px-2 py-1">
|
|
218
|
+
<Toast.Title data-slot="toast-title" />
|
|
219
|
+
</Toast.Content>
|
|
220
|
+
) : (
|
|
221
|
+
<Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
|
|
222
|
+
<div className="flex gap-2">
|
|
223
|
+
{Icon && (
|
|
224
|
+
<div
|
|
225
|
+
className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
|
226
|
+
data-slot="toast-icon"
|
|
227
|
+
>
|
|
228
|
+
<Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
<div className="flex flex-col gap-0.5">
|
|
233
|
+
<Toast.Title
|
|
234
|
+
className="font-medium"
|
|
235
|
+
data-slot="toast-title"
|
|
236
|
+
/>
|
|
237
|
+
<Toast.Description
|
|
238
|
+
className="text-muted-foreground"
|
|
239
|
+
data-slot="toast-description"
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
{toast.actionProps && (
|
|
244
|
+
<Toast.Action
|
|
245
|
+
className={buttonVariants({ size: "xs" })}
|
|
246
|
+
data-slot="toast-action"
|
|
247
|
+
>
|
|
248
|
+
{toast.actionProps.children}
|
|
249
|
+
</Toast.Action>
|
|
250
|
+
)}
|
|
251
|
+
</Toast.Content>
|
|
252
|
+
)}
|
|
253
|
+
</Toast.Root>
|
|
254
|
+
</Toast.Positioner>
|
|
255
|
+
);
|
|
256
|
+
})}
|
|
257
|
+
</Toast.Viewport>
|
|
258
|
+
</Toast.Portal>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export {
|
|
263
|
+
ToastProvider,
|
|
264
|
+
type ToastPosition,
|
|
265
|
+
toastManager,
|
|
266
|
+
AnchoredToastProvider,
|
|
267
|
+
anchoredToastManager,
|
|
268
|
+
};
|