@voila.dev/ui 1.1.9
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/package.json +78 -0
- package/src/components/ui/accordion.tsx +79 -0
- package/src/components/ui/alert-dialog.tsx +198 -0
- package/src/components/ui/alert.tsx +105 -0
- package/src/components/ui/aspect-ratio.tsx +30 -0
- package/src/components/ui/avatar.tsx +125 -0
- package/src/components/ui/badge-variants.ts +166 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/banner.tsx +84 -0
- package/src/components/ui/breadcrumb.tsx +127 -0
- package/src/components/ui/button-group.tsx +99 -0
- package/src/components/ui/button-variants.ts +108 -0
- package/src/components/ui/button.tsx +54 -0
- package/src/components/ui/calendar.tsx +266 -0
- package/src/components/ui/card.tsx +155 -0
- package/src/components/ui/carousel.tsx +319 -0
- package/src/components/ui/chat.tsx +896 -0
- package/src/components/ui/checkbox-group.tsx +44 -0
- package/src/components/ui/checkbox.tsx +34 -0
- package/src/components/ui/chip.tsx +60 -0
- package/src/components/ui/collapsible.tsx +44 -0
- package/src/components/ui/color-picker.tsx +191 -0
- package/src/components/ui/combobox.tsx +317 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/confirm-dialog.tsx +125 -0
- package/src/components/ui/context-menu.tsx +254 -0
- package/src/components/ui/copyable-text.tsx +65 -0
- package/src/components/ui/date-picker.tsx +316 -0
- package/src/components/ui/date-time-picker.tsx +1071 -0
- package/src/components/ui/dialog.tsx +176 -0
- package/src/components/ui/direction.tsx +15 -0
- package/src/components/ui/drawer.tsx +178 -0
- package/src/components/ui/dropdown-menu.tsx +247 -0
- package/src/components/ui/empty.tsx +131 -0
- package/src/components/ui/field.tsx +257 -0
- package/src/components/ui/formatted-input.tsx +219 -0
- package/src/components/ui/gallery.tsx +161 -0
- package/src/components/ui/hover-card.tsx +77 -0
- package/src/components/ui/icon-picker.tsx +193 -0
- package/src/components/ui/image-cropper.tsx +787 -0
- package/src/components/ui/image-upload-field.tsx +344 -0
- package/src/components/ui/input-group.tsx +175 -0
- package/src/components/ui/input-otp.tsx +83 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +212 -0
- package/src/components/ui/kbd.tsx +62 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/list.tsx +46 -0
- package/src/components/ui/menu-variants.ts +71 -0
- package/src/components/ui/menubar.tsx +226 -0
- package/src/components/ui/money-input.tsx +96 -0
- package/src/components/ui/native-date-picker.tsx +120 -0
- package/src/components/ui/native-select.tsx +74 -0
- package/src/components/ui/navigation-menu.tsx +183 -0
- package/src/components/ui/pagination.tsx +150 -0
- package/src/components/ui/popover.tsx +104 -0
- package/src/components/ui/profile-header.tsx +196 -0
- package/src/components/ui/progress.tsx +71 -0
- package/src/components/ui/radio-group.tsx +115 -0
- package/src/components/ui/rating.tsx +200 -0
- package/src/components/ui/resizable.tsx +61 -0
- package/src/components/ui/responsive-dialog.tsx +278 -0
- package/src/components/ui/responsive-select.tsx +289 -0
- package/src/components/ui/responsive-sheet.tsx +225 -0
- package/src/components/ui/scroll-area.tsx +51 -0
- package/src/components/ui/section.tsx +106 -0
- package/src/components/ui/segmented-control.tsx +148 -0
- package/src/components/ui/select.tsx +216 -0
- package/src/components/ui/separator.tsx +69 -0
- package/src/components/ui/sheet.tsx +154 -0
- package/src/components/ui/shortcut.tsx +41 -0
- package/src/components/ui/sidebar.tsx +739 -0
- package/src/components/ui/skeleton.tsx +23 -0
- package/src/components/ui/slider.tsx +74 -0
- package/src/components/ui/sonner.tsx +64 -0
- package/src/components/ui/spinner.tsx +19 -0
- package/src/components/ui/stat-card.tsx +138 -0
- package/src/components/ui/stepper.tsx +181 -0
- package/src/components/ui/sticky-action-bar.tsx +42 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +132 -0
- package/src/components/ui/tabs.tsx +77 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/time-picker.tsx +221 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +79 -0
- package/src/components/ui/translation-input.tsx +126 -0
- package/src/components/ui/user-avatar.tsx +74 -0
- package/src/css-modules.d.ts +4 -0
- package/src/hooks/use-command-palette.ts +28 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-pagination.ts +81 -0
- package/src/hooks/use-picker-state.ts +44 -0
- package/src/lib/cva.ts +18 -0
- package/src/lib/time-math.ts +62 -0
- package/src/lib/utils.ts +14 -0
- package/src/styles/globals.css +36 -0
- package/src/styles.css +10 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { CalendarBlankIcon } from "@phosphor-icons/react";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
import type { DateRange } from "react-day-picker";
|
|
4
|
+
import { Button, type ButtonProps } from "#/components/ui/button.tsx";
|
|
5
|
+
import { Calendar } from "#/components/ui/calendar.tsx";
|
|
6
|
+
import {
|
|
7
|
+
Popover,
|
|
8
|
+
PopoverContent,
|
|
9
|
+
PopoverTrigger,
|
|
10
|
+
} from "#/components/ui/popover.tsx";
|
|
11
|
+
import { usePickerState } from "#/hooks/use-picker-state.ts";
|
|
12
|
+
import { cn } from "#/lib/utils.ts";
|
|
13
|
+
|
|
14
|
+
type CalendarPassthroughProps = Omit<
|
|
15
|
+
React.ComponentProps<typeof Calendar>,
|
|
16
|
+
"mode" | "selected" | "onSelect" | "locale"
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
/** Long localized date for the trigger label (e.g. "June 12, 2026" / "12 juin 2026"). */
|
|
20
|
+
const DEFAULT_DATE_FORMAT: Intl.DateTimeFormatOptions = { dateStyle: "long" };
|
|
21
|
+
|
|
22
|
+
/** Local `yyyy-MM-dd` for the hidden form input — the shape a `<input type=date>` wants. */
|
|
23
|
+
function toIsoDay(date: Date): string {
|
|
24
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
25
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
26
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type DatePickerBaseProps = {
|
|
30
|
+
placeholder?: string;
|
|
31
|
+
/**
|
|
32
|
+
* `Intl.DateTimeFormat` options for the trigger label. Defaults to a long
|
|
33
|
+
* localized date (`{ dateStyle: "long" }`, e.g. "June 12, 2026" / "12 juin 2026").
|
|
34
|
+
*/
|
|
35
|
+
formatOptions?: Intl.DateTimeFormatOptions;
|
|
36
|
+
/** BCP-47 locale (e.g. "fr-FR"), applied to both the trigger label and the calendar. */
|
|
37
|
+
locale?: string;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Name for the hidden form input(s); value(s) serialized as yyyy-MM-dd. */
|
|
40
|
+
name?: string;
|
|
41
|
+
id?: string;
|
|
42
|
+
className?: string;
|
|
43
|
+
"aria-invalid"?: React.AriaAttributes["aria-invalid"];
|
|
44
|
+
"aria-label"?: string;
|
|
45
|
+
variant?: ButtonProps["variant"];
|
|
46
|
+
/** Escape hatch for the underlying Calendar (disabled days, week numbers…). */
|
|
47
|
+
calendarProps?: CalendarPassthroughProps;
|
|
48
|
+
defaultOpen?: boolean;
|
|
49
|
+
open?: boolean;
|
|
50
|
+
onOpenChange?: (open: boolean) => void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The shared input-look trigger for the popover pickers (DatePicker,
|
|
55
|
+
* DateRangePicker, TimePicker): leading icon + label or muted placeholder.
|
|
56
|
+
*/
|
|
57
|
+
function DatePickerTrigger({
|
|
58
|
+
className,
|
|
59
|
+
empty,
|
|
60
|
+
children,
|
|
61
|
+
// An explicit `variant: undefined` in the spread would override the inner
|
|
62
|
+
// Button's JSX default, so the fallback must live here.
|
|
63
|
+
variant = "outline",
|
|
64
|
+
icon = (
|
|
65
|
+
<CalendarBlankIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
66
|
+
),
|
|
67
|
+
slot = "date-picker-trigger",
|
|
68
|
+
...props
|
|
69
|
+
}: ButtonProps & { empty: boolean; icon?: React.ReactNode; slot?: string }) {
|
|
70
|
+
return (
|
|
71
|
+
<PopoverTrigger
|
|
72
|
+
data-slot={slot}
|
|
73
|
+
data-empty={empty || undefined}
|
|
74
|
+
render={
|
|
75
|
+
<Button
|
|
76
|
+
variant={variant}
|
|
77
|
+
className={cn(
|
|
78
|
+
"min-w-48 justify-start text-start font-normal data-empty:text-muted-foreground",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
{icon}
|
|
86
|
+
<span className="truncate">{children}</span>
|
|
87
|
+
</PopoverTrigger>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function DatePickerHiddenInput({
|
|
92
|
+
name,
|
|
93
|
+
date,
|
|
94
|
+
}: {
|
|
95
|
+
name?: string;
|
|
96
|
+
date: Date | null | undefined;
|
|
97
|
+
}) {
|
|
98
|
+
if (!name) return null;
|
|
99
|
+
return <input type="hidden" name={name} value={date ? toIsoDay(date) : ""} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function DatePickerContent({ children }: { children: React.ReactNode }) {
|
|
103
|
+
return (
|
|
104
|
+
<PopoverContent
|
|
105
|
+
data-slot="date-picker-content"
|
|
106
|
+
className="w-auto p-0"
|
|
107
|
+
align="start"
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</PopoverContent>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatDateLabel(
|
|
115
|
+
value: Date | null | undefined,
|
|
116
|
+
locale: string | undefined,
|
|
117
|
+
formatOptions: Intl.DateTimeFormatOptions,
|
|
118
|
+
placeholder: string,
|
|
119
|
+
): string {
|
|
120
|
+
if (!value) {
|
|
121
|
+
return placeholder;
|
|
122
|
+
}
|
|
123
|
+
return new Intl.DateTimeFormat(locale, formatOptions).format(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function DatePicker({
|
|
127
|
+
value: controlledValue,
|
|
128
|
+
defaultValue,
|
|
129
|
+
onValueChange,
|
|
130
|
+
placeholder = "Pick a date",
|
|
131
|
+
formatOptions = DEFAULT_DATE_FORMAT,
|
|
132
|
+
locale,
|
|
133
|
+
disabled = false,
|
|
134
|
+
name,
|
|
135
|
+
id,
|
|
136
|
+
className,
|
|
137
|
+
"aria-invalid": ariaInvalid,
|
|
138
|
+
"aria-label": ariaLabel,
|
|
139
|
+
variant,
|
|
140
|
+
calendarProps,
|
|
141
|
+
defaultOpen,
|
|
142
|
+
open: controlledOpen,
|
|
143
|
+
onOpenChange,
|
|
144
|
+
}: DatePickerBaseProps & {
|
|
145
|
+
/** Controlled value; pass `null` for a controlled empty selection. */
|
|
146
|
+
value?: Date | null;
|
|
147
|
+
defaultValue?: Date;
|
|
148
|
+
onValueChange?: (date: Date | null) => void;
|
|
149
|
+
}) {
|
|
150
|
+
const { isControlled, value, setUncontrolledValue, open, setOpen } =
|
|
151
|
+
usePickerState<Date>({
|
|
152
|
+
value: controlledValue,
|
|
153
|
+
defaultValue,
|
|
154
|
+
open: controlledOpen,
|
|
155
|
+
defaultOpen,
|
|
156
|
+
onOpenChange,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const handleSelect = (date: Date | undefined) => {
|
|
160
|
+
if (!isControlled) setUncontrolledValue(date);
|
|
161
|
+
onValueChange?.(date ?? null);
|
|
162
|
+
// Single selection is complete the moment a day is picked.
|
|
163
|
+
if (date) setOpen(false);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const selectedDate = value ?? undefined;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
170
|
+
<DatePickerTrigger
|
|
171
|
+
id={id}
|
|
172
|
+
className={className}
|
|
173
|
+
disabled={disabled}
|
|
174
|
+
empty={!value}
|
|
175
|
+
aria-invalid={ariaInvalid}
|
|
176
|
+
aria-label={ariaLabel}
|
|
177
|
+
variant={variant}
|
|
178
|
+
>
|
|
179
|
+
{formatDateLabel(value, locale, formatOptions, placeholder)}
|
|
180
|
+
</DatePickerTrigger>
|
|
181
|
+
<DatePickerHiddenInput name={name} date={value} />
|
|
182
|
+
<DatePickerContent>
|
|
183
|
+
<Calendar
|
|
184
|
+
mode="single"
|
|
185
|
+
selected={selectedDate}
|
|
186
|
+
onSelect={handleSelect}
|
|
187
|
+
defaultMonth={selectedDate}
|
|
188
|
+
locale={locale}
|
|
189
|
+
autoFocus
|
|
190
|
+
{...calendarProps}
|
|
191
|
+
/>
|
|
192
|
+
</DatePickerContent>
|
|
193
|
+
</Popover>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A range is complete once both ends differ; the first click yields
|
|
198
|
+
// from === to, which must keep the popover open for the second click.
|
|
199
|
+
function isRangeSelectionComplete(range: DateRange | undefined): boolean {
|
|
200
|
+
if (!range?.from || !range.to) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return range.from.getTime() !== range.to.getTime();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// A same-day range (also the intermediate state after the first click)
|
|
207
|
+
// collapses to a single date instead of "June 9 – June 9".
|
|
208
|
+
function formatDateRangeLabel(
|
|
209
|
+
value: DateRange | null | undefined,
|
|
210
|
+
locale: string | undefined,
|
|
211
|
+
formatOptions: Intl.DateTimeFormatOptions,
|
|
212
|
+
placeholder: string,
|
|
213
|
+
): string {
|
|
214
|
+
if (!value?.from) {
|
|
215
|
+
return placeholder;
|
|
216
|
+
}
|
|
217
|
+
const dateFormat = new Intl.DateTimeFormat(locale, formatOptions);
|
|
218
|
+
if (!value.to || value.to.getTime() === value.from.getTime()) {
|
|
219
|
+
return dateFormat.format(value.from);
|
|
220
|
+
}
|
|
221
|
+
return `${dateFormat.format(value.from)} – ${dateFormat.format(value.to)}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function DateRangePickerHiddenInputs({
|
|
225
|
+
name,
|
|
226
|
+
value,
|
|
227
|
+
}: {
|
|
228
|
+
name?: string;
|
|
229
|
+
value: DateRange | null | undefined;
|
|
230
|
+
}) {
|
|
231
|
+
if (!name) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return (
|
|
235
|
+
<>
|
|
236
|
+
<DatePickerHiddenInput name={`${name}-from`} date={value?.from} />
|
|
237
|
+
<DatePickerHiddenInput name={`${name}-to`} date={value?.to} />
|
|
238
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function DateRangePicker({
|
|
243
|
+
value: controlledValue,
|
|
244
|
+
defaultValue,
|
|
245
|
+
onValueChange,
|
|
246
|
+
placeholder = "Pick a date range",
|
|
247
|
+
formatOptions = DEFAULT_DATE_FORMAT,
|
|
248
|
+
locale,
|
|
249
|
+
disabled = false,
|
|
250
|
+
name,
|
|
251
|
+
id,
|
|
252
|
+
className,
|
|
253
|
+
"aria-invalid": ariaInvalid,
|
|
254
|
+
"aria-label": ariaLabel,
|
|
255
|
+
variant,
|
|
256
|
+
calendarProps,
|
|
257
|
+
defaultOpen,
|
|
258
|
+
open: controlledOpen,
|
|
259
|
+
onOpenChange,
|
|
260
|
+
}: DatePickerBaseProps & {
|
|
261
|
+
/** Controlled value; pass `null` for a controlled empty selection. */
|
|
262
|
+
value?: DateRange | null;
|
|
263
|
+
defaultValue?: DateRange;
|
|
264
|
+
onValueChange?: (range: DateRange | null) => void;
|
|
265
|
+
}) {
|
|
266
|
+
const { isControlled, value, setUncontrolledValue, open, setOpen } =
|
|
267
|
+
usePickerState<DateRange>({
|
|
268
|
+
value: controlledValue,
|
|
269
|
+
defaultValue,
|
|
270
|
+
open: controlledOpen,
|
|
271
|
+
defaultOpen,
|
|
272
|
+
onOpenChange,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const handleSelect = (range: DateRange | undefined) => {
|
|
276
|
+
if (!isControlled) setUncontrolledValue(range);
|
|
277
|
+
onValueChange?.(range ?? null);
|
|
278
|
+
if (isRangeSelectionComplete(range)) {
|
|
279
|
+
setOpen(false);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const selectedRange = value ?? undefined;
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
287
|
+
<DatePickerTrigger
|
|
288
|
+
id={id}
|
|
289
|
+
className={cn("min-w-64", className)}
|
|
290
|
+
disabled={disabled}
|
|
291
|
+
empty={!value?.from}
|
|
292
|
+
aria-invalid={ariaInvalid}
|
|
293
|
+
aria-label={ariaLabel}
|
|
294
|
+
variant={variant}
|
|
295
|
+
>
|
|
296
|
+
{formatDateRangeLabel(value, locale, formatOptions, placeholder)}
|
|
297
|
+
</DatePickerTrigger>
|
|
298
|
+
<DateRangePickerHiddenInputs name={name} value={value} />
|
|
299
|
+
<DatePickerContent>
|
|
300
|
+
<Calendar
|
|
301
|
+
mode="range"
|
|
302
|
+
numberOfMonths={2}
|
|
303
|
+
selected={selectedRange}
|
|
304
|
+
onSelect={handleSelect}
|
|
305
|
+
defaultMonth={selectedRange?.from}
|
|
306
|
+
locale={locale}
|
|
307
|
+
autoFocus
|
|
308
|
+
{...calendarProps}
|
|
309
|
+
/>
|
|
310
|
+
</DatePickerContent>
|
|
311
|
+
</Popover>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export type { DateRange };
|
|
316
|
+
export { DatePicker, DatePickerTrigger, DateRangePicker };
|