@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.
Files changed (99) hide show
  1. package/package.json +78 -0
  2. package/src/components/ui/accordion.tsx +79 -0
  3. package/src/components/ui/alert-dialog.tsx +198 -0
  4. package/src/components/ui/alert.tsx +105 -0
  5. package/src/components/ui/aspect-ratio.tsx +30 -0
  6. package/src/components/ui/avatar.tsx +125 -0
  7. package/src/components/ui/badge-variants.ts +166 -0
  8. package/src/components/ui/badge.tsx +45 -0
  9. package/src/components/ui/banner.tsx +84 -0
  10. package/src/components/ui/breadcrumb.tsx +127 -0
  11. package/src/components/ui/button-group.tsx +99 -0
  12. package/src/components/ui/button-variants.ts +108 -0
  13. package/src/components/ui/button.tsx +54 -0
  14. package/src/components/ui/calendar.tsx +266 -0
  15. package/src/components/ui/card.tsx +155 -0
  16. package/src/components/ui/carousel.tsx +319 -0
  17. package/src/components/ui/chat.tsx +896 -0
  18. package/src/components/ui/checkbox-group.tsx +44 -0
  19. package/src/components/ui/checkbox.tsx +34 -0
  20. package/src/components/ui/chip.tsx +60 -0
  21. package/src/components/ui/collapsible.tsx +44 -0
  22. package/src/components/ui/color-picker.tsx +191 -0
  23. package/src/components/ui/combobox.tsx +317 -0
  24. package/src/components/ui/command.tsx +187 -0
  25. package/src/components/ui/confirm-dialog.tsx +125 -0
  26. package/src/components/ui/context-menu.tsx +254 -0
  27. package/src/components/ui/copyable-text.tsx +65 -0
  28. package/src/components/ui/date-picker.tsx +316 -0
  29. package/src/components/ui/date-time-picker.tsx +1071 -0
  30. package/src/components/ui/dialog.tsx +176 -0
  31. package/src/components/ui/direction.tsx +15 -0
  32. package/src/components/ui/drawer.tsx +178 -0
  33. package/src/components/ui/dropdown-menu.tsx +247 -0
  34. package/src/components/ui/empty.tsx +131 -0
  35. package/src/components/ui/field.tsx +257 -0
  36. package/src/components/ui/formatted-input.tsx +219 -0
  37. package/src/components/ui/gallery.tsx +161 -0
  38. package/src/components/ui/hover-card.tsx +77 -0
  39. package/src/components/ui/icon-picker.tsx +193 -0
  40. package/src/components/ui/image-cropper.tsx +787 -0
  41. package/src/components/ui/image-upload-field.tsx +344 -0
  42. package/src/components/ui/input-group.tsx +175 -0
  43. package/src/components/ui/input-otp.tsx +83 -0
  44. package/src/components/ui/input.tsx +21 -0
  45. package/src/components/ui/item.tsx +212 -0
  46. package/src/components/ui/kbd.tsx +62 -0
  47. package/src/components/ui/label.tsx +19 -0
  48. package/src/components/ui/list.tsx +46 -0
  49. package/src/components/ui/menu-variants.ts +71 -0
  50. package/src/components/ui/menubar.tsx +226 -0
  51. package/src/components/ui/money-input.tsx +96 -0
  52. package/src/components/ui/native-date-picker.tsx +120 -0
  53. package/src/components/ui/native-select.tsx +74 -0
  54. package/src/components/ui/navigation-menu.tsx +183 -0
  55. package/src/components/ui/pagination.tsx +150 -0
  56. package/src/components/ui/popover.tsx +104 -0
  57. package/src/components/ui/profile-header.tsx +196 -0
  58. package/src/components/ui/progress.tsx +71 -0
  59. package/src/components/ui/radio-group.tsx +115 -0
  60. package/src/components/ui/rating.tsx +200 -0
  61. package/src/components/ui/resizable.tsx +61 -0
  62. package/src/components/ui/responsive-dialog.tsx +278 -0
  63. package/src/components/ui/responsive-select.tsx +289 -0
  64. package/src/components/ui/responsive-sheet.tsx +225 -0
  65. package/src/components/ui/scroll-area.tsx +51 -0
  66. package/src/components/ui/section.tsx +106 -0
  67. package/src/components/ui/segmented-control.tsx +148 -0
  68. package/src/components/ui/select.tsx +216 -0
  69. package/src/components/ui/separator.tsx +69 -0
  70. package/src/components/ui/sheet.tsx +154 -0
  71. package/src/components/ui/shortcut.tsx +41 -0
  72. package/src/components/ui/sidebar.tsx +739 -0
  73. package/src/components/ui/skeleton.tsx +23 -0
  74. package/src/components/ui/slider.tsx +74 -0
  75. package/src/components/ui/sonner.tsx +64 -0
  76. package/src/components/ui/spinner.tsx +19 -0
  77. package/src/components/ui/stat-card.tsx +138 -0
  78. package/src/components/ui/stepper.tsx +181 -0
  79. package/src/components/ui/sticky-action-bar.tsx +42 -0
  80. package/src/components/ui/switch.tsx +30 -0
  81. package/src/components/ui/table.tsx +132 -0
  82. package/src/components/ui/tabs.tsx +77 -0
  83. package/src/components/ui/textarea.tsx +24 -0
  84. package/src/components/ui/time-picker.tsx +221 -0
  85. package/src/components/ui/toggle-group.tsx +86 -0
  86. package/src/components/ui/toggle.tsx +43 -0
  87. package/src/components/ui/tooltip.tsx +79 -0
  88. package/src/components/ui/translation-input.tsx +126 -0
  89. package/src/components/ui/user-avatar.tsx +74 -0
  90. package/src/css-modules.d.ts +4 -0
  91. package/src/hooks/use-command-palette.ts +28 -0
  92. package/src/hooks/use-mobile.ts +21 -0
  93. package/src/hooks/use-pagination.ts +81 -0
  94. package/src/hooks/use-picker-state.ts +44 -0
  95. package/src/lib/cva.ts +18 -0
  96. package/src/lib/time-math.ts +62 -0
  97. package/src/lib/utils.ts +14 -0
  98. package/src/styles/globals.css +36 -0
  99. 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 };