@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,132 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ /**
6
+ * Wraps the `<table>` in a scroll container so wide tables pan horizontally
7
+ * instead of breaking the page layout. Style the container itself (max-height,
8
+ * sticky headers, borders) through `containerClassName`.
9
+ */
10
+ function Table({
11
+ className,
12
+ containerClassName,
13
+ ...props
14
+ }: React.ComponentProps<"table"> & { containerClassName?: string }) {
15
+ return (
16
+ <div
17
+ data-slot="table-container"
18
+ className={cn("relative w-full overflow-x-auto", containerClassName)}
19
+ >
20
+ <table
21
+ data-slot="table"
22
+ className={cn("w-full caption-bottom text-sm", className)}
23
+ {...props}
24
+ />
25
+ </div>
26
+ );
27
+ }
28
+
29
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
30
+ return (
31
+ <thead
32
+ data-slot="table-header"
33
+ className={cn("[&_tr]:border-b", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
40
+ return (
41
+ <tbody
42
+ data-slot="table-body"
43
+ className={cn("[&_tr:last-child]:border-0", className)}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
50
+ return (
51
+ <tfoot
52
+ data-slot="table-footer"
53
+ className={cn(
54
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
55
+ className,
56
+ )}
57
+ {...props}
58
+ />
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Hover highlighting only applies to body rows, not header/footer rows. Stamp
64
+ * `data-selected` on a row to mark it as selected.
65
+ */
66
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
67
+ return (
68
+ <tr
69
+ data-slot="table-row"
70
+ className={cn(
71
+ "border-b transition-colors has-aria-expanded:bg-muted/50 data-selected:bg-muted [tbody_&]:hover:bg-muted/50",
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
80
+ return (
81
+ <th
82
+ data-slot="table-head"
83
+ className={cn(
84
+ "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
85
+ className,
86
+ )}
87
+ {...props}
88
+ />
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Cells don't wrap by default - long content pans the table horizontally via
94
+ * the scroll container. Pass `whitespace-normal` for columns that should wrap
95
+ * (e.g. descriptions).
96
+ */
97
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
98
+ return (
99
+ <td
100
+ data-slot="table-cell"
101
+ className={cn(
102
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
103
+ className,
104
+ )}
105
+ {...props}
106
+ />
107
+ );
108
+ }
109
+
110
+ function TableCaption({
111
+ className,
112
+ ...props
113
+ }: React.ComponentProps<"caption">) {
114
+ return (
115
+ <caption
116
+ data-slot="table-caption"
117
+ className={cn("mt-4 text-sm text-muted-foreground", className)}
118
+ {...props}
119
+ />
120
+ );
121
+ }
122
+
123
+ export {
124
+ Table,
125
+ TableBody,
126
+ TableCaption,
127
+ TableCell,
128
+ TableFooter,
129
+ TableHead,
130
+ TableHeader,
131
+ TableRow,
132
+ };
@@ -0,0 +1,77 @@
1
+ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
2
+ import { cva, type VariantProps } from "#/lib/cva.ts";
3
+
4
+ import { cn } from "#/lib/utils.ts";
5
+
6
+ function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
7
+ return (
8
+ <TabsPrimitive.Root
9
+ data-slot="tabs"
10
+ className={cn(
11
+ "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ const tabsListVariants = cva({
20
+ base: "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
21
+ variants: {
22
+ variant: {
23
+ default: "bg-muted",
24
+ line: "gap-1 bg-transparent",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: "default",
29
+ },
30
+ });
31
+
32
+ function TabsList({
33
+ className,
34
+ variant = "default",
35
+ ...props
36
+ }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
37
+ return (
38
+ <TabsPrimitive.List
39
+ data-slot="tabs-list"
40
+ data-variant={variant}
41
+ className={cn(tabsListVariants({ variant }), className)}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
48
+ return (
49
+ <TabsPrimitive.Tab
50
+ data-slot="tabs-trigger"
51
+ className={cn(
52
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-disabled:pointer-events-none data-disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
53
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
54
+ "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
55
+ "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+
63
+ function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
64
+ return (
65
+ <TabsPrimitive.Panel
66
+ data-slot="tabs-content"
67
+ className={cn(
68
+ // Inactive panels are unmounted, so the fade runs on each switch.
69
+ "flex-1 animate-in text-sm outline-none fade-in-0 motion-reduce:animate-none!",
70
+ className,
71
+ )}
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ export { Tabs, TabsContent, TabsList, TabsTrigger, tabsListVariants };
@@ -0,0 +1,24 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ /**
6
+ * Auto-grows with content (`field-sizing-content`) between `min-h-16` and
7
+ * `max-h-64`, then scrolls. Note that content sizing makes the `rows`
8
+ * attribute inert - size a taller or shorter field with `min-h-*`/`max-h-*`
9
+ * through `className` instead.
10
+ */
11
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
12
+ return (
13
+ <textarea
14
+ data-slot="textarea"
15
+ className={cn(
16
+ "field-sizing-content max-h-64 min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-[color,background-color,border-color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:scheme-dark dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ export { Textarea };
@@ -0,0 +1,221 @@
1
+ import { ClockIcon } from "@phosphor-icons/react";
2
+ import * as React from "react";
3
+ import { Button, type ButtonProps } from "#/components/ui/button.tsx";
4
+ import { DatePickerTrigger } from "#/components/ui/date-picker.tsx";
5
+ import { Popover, PopoverContent } from "#/components/ui/popover.tsx";
6
+ import { usePickerState } from "#/hooks/use-picker-state.ts";
7
+ import {
8
+ formatMinutesLabel,
9
+ minutesToTimeValue,
10
+ parseTimeToMinutes,
11
+ } from "#/lib/time-math.ts";
12
+ import { cn } from "#/lib/utils.ts";
13
+
14
+ function formatTimeLabel(
15
+ time: string,
16
+ locale: string | undefined,
17
+ options: Intl.DateTimeFormatOptions | undefined,
18
+ ): string {
19
+ const totalMinutes = parseTimeToMinutes(time);
20
+ // An unparseable value is the caller's literal string; show it as-is.
21
+ if (totalMinutes === null) return time;
22
+ return formatMinutesLabel(totalMinutes, locale, options);
23
+ }
24
+
25
+ function timeRangeBoundsInMinutes(
26
+ min: string,
27
+ max: string,
28
+ ): { first: number; last: number } {
29
+ return {
30
+ first: parseTimeToMinutes(min) ?? 0,
31
+ last: parseTimeToMinutes(max) ?? 23 * 60 + 59,
32
+ };
33
+ }
34
+
35
+ function timeOptionValues(min: string, max: string, step: number): string[] {
36
+ const stepMinutes = step > 0 ? step : 30;
37
+ const { first, last } = timeRangeBoundsInMinutes(min, max);
38
+ const times: string[] = [];
39
+ for (let minutes = first; minutes <= last; minutes += stepMinutes) {
40
+ times.push(minutesToTimeValue(minutes));
41
+ }
42
+ return times;
43
+ }
44
+
45
+ /** The hidden form input serializing the selection as HH:mm (none without a name). */
46
+ function HiddenTimeInput({
47
+ name,
48
+ value,
49
+ }: {
50
+ name: string | undefined;
51
+ value: string | null | undefined;
52
+ }) {
53
+ if (!name) return null;
54
+ return <input type="hidden" name={name} value={value ?? ""} />;
55
+ }
56
+
57
+ function TimePickerOption({
58
+ time,
59
+ label,
60
+ selected,
61
+ selectedOptionRef,
62
+ onSelect,
63
+ }: {
64
+ time: string;
65
+ label: string;
66
+ selected: boolean;
67
+ selectedOptionRef: React.Ref<HTMLButtonElement>;
68
+ onSelect: (time: string) => void;
69
+ }) {
70
+ return (
71
+ <Button
72
+ role="option"
73
+ aria-selected={selected}
74
+ data-selected={selected || undefined}
75
+ variant="ghost"
76
+ size="sm"
77
+ className="shrink-0 justify-center font-normal data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:bg-primary data-selected:hover:text-primary-foreground"
78
+ ref={selected ? selectedOptionRef : undefined}
79
+ onClick={() => onSelect(time)}
80
+ >
81
+ {label}
82
+ </Button>
83
+ );
84
+ }
85
+
86
+ function TimePicker(props: {
87
+ /** Controlled "HH:mm" value; pass `null` for a controlled empty selection. */
88
+ value?: string | null;
89
+ defaultValue?: string;
90
+ onValueChange?: (time: string | null) => void;
91
+ placeholder?: string;
92
+ /**
93
+ * `Intl.DateTimeFormat` options for the labels. Defaults to a short localized
94
+ * time (`{ hour: "numeric", minute: "2-digit" }`, e.g. "2:30 PM" / "14:30").
95
+ */
96
+ formatOptions?: Intl.DateTimeFormatOptions;
97
+ /** BCP-47 locale (e.g. "fr-FR"), applied to the trigger and option labels. */
98
+ locale?: string;
99
+ disabled?: boolean;
100
+ /** Minutes between two options. Defaults to 30. */
101
+ step?: number;
102
+ /** First selectable time, "HH:mm" inclusive. */
103
+ min?: string;
104
+ /** Last selectable time, "HH:mm" inclusive. */
105
+ max?: string;
106
+ /** Name for the hidden form input; the value is serialized as HH:mm. */
107
+ name?: string;
108
+ id?: string;
109
+ className?: string;
110
+ "aria-invalid"?: React.AriaAttributes["aria-invalid"];
111
+ "aria-label"?: string;
112
+ variant?: ButtonProps["variant"];
113
+ defaultOpen?: boolean;
114
+ open?: boolean;
115
+ onOpenChange?: (open: boolean) => void;
116
+ }) {
117
+ const {
118
+ value: controlledValue,
119
+ defaultValue,
120
+ onValueChange,
121
+ placeholder = "Pick a time",
122
+ formatOptions,
123
+ locale,
124
+ disabled = false,
125
+ step = 30,
126
+ min = "00:00",
127
+ max = "23:59",
128
+ name,
129
+ id,
130
+ className,
131
+ "aria-invalid": ariaInvalid,
132
+ "aria-label": ariaLabel,
133
+ variant,
134
+ defaultOpen,
135
+ open: controlledOpen,
136
+ onOpenChange,
137
+ } = props;
138
+
139
+ const { isControlled, value, setUncontrolledValue, open, setOpen } =
140
+ usePickerState<string>({
141
+ value: controlledValue,
142
+ defaultValue,
143
+ open: controlledOpen,
144
+ defaultOpen,
145
+ onOpenChange,
146
+ });
147
+
148
+ const options = React.useMemo(
149
+ () => timeOptionValues(min, max, step),
150
+ [step, min, max],
151
+ );
152
+
153
+ const selectedMinutes = parseTimeToMinutes(value);
154
+
155
+ const handleSelect = (time: string) => {
156
+ if (!isControlled) setUncontrolledValue(time);
157
+ onValueChange?.(time);
158
+ setOpen(false);
159
+ };
160
+
161
+ // The selected option receives the popup's initial focus (otherwise Base UI
162
+ // focuses the first option, scrolling the list back to the top) and is then
163
+ // centered once the popup has been positioned — the ref fires too early, so
164
+ // defer by a frame. (Optional call: jsdom has no scrollIntoView.)
165
+ const selectedOptionRef = React.useRef<HTMLButtonElement | null>(null);
166
+ const setSelectedOption = React.useCallback(
167
+ (node: HTMLButtonElement | null) => {
168
+ selectedOptionRef.current = node;
169
+ if (!node) return;
170
+ requestAnimationFrame(() => node.scrollIntoView?.({ block: "center" }));
171
+ },
172
+ [],
173
+ );
174
+
175
+ return (
176
+ <Popover open={open} onOpenChange={setOpen}>
177
+ <DatePickerTrigger
178
+ slot="time-picker-trigger"
179
+ icon={<ClockIcon className="size-4 shrink-0 text-muted-foreground" />}
180
+ id={id}
181
+ className={cn("min-w-32", className)}
182
+ disabled={disabled}
183
+ empty={!value}
184
+ aria-invalid={ariaInvalid}
185
+ aria-label={ariaLabel}
186
+ variant={variant}
187
+ >
188
+ {value ? formatTimeLabel(value, locale, formatOptions) : placeholder}
189
+ </DatePickerTrigger>
190
+ <HiddenTimeInput name={name} value={value} />
191
+ <PopoverContent
192
+ data-slot="time-picker-content"
193
+ className="w-auto min-w-(--anchor-width) p-1"
194
+ align="start"
195
+ initialFocus={selectedMinutes !== null ? selectedOptionRef : undefined}
196
+ >
197
+ <div
198
+ role="listbox"
199
+ aria-label={ariaLabel ?? placeholder}
200
+ className="flex max-h-64 flex-col gap-0.5 overflow-y-auto overscroll-contain"
201
+ >
202
+ {options.map((time) => (
203
+ <TimePickerOption
204
+ key={time}
205
+ time={time}
206
+ label={formatTimeLabel(time, locale, formatOptions)}
207
+ selected={
208
+ selectedMinutes !== null &&
209
+ parseTimeToMinutes(time) === selectedMinutes
210
+ }
211
+ selectedOptionRef={setSelectedOption}
212
+ onSelect={handleSelect}
213
+ />
214
+ ))}
215
+ </div>
216
+ </PopoverContent>
217
+ </Popover>
218
+ );
219
+ }
220
+
221
+ export { TimePicker };
@@ -0,0 +1,86 @@
1
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
2
+ import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
3
+ import * as React from "react";
4
+ import { toggleVariants } from "#/components/ui/toggle.tsx";
5
+ import type { VariantProps } from "#/lib/cva.ts";
6
+ import { cn } from "#/lib/utils.ts";
7
+
8
+ const ToggleGroupContext = React.createContext<
9
+ VariantProps<typeof toggleVariants> & {
10
+ spacing?: number;
11
+ orientation?: "horizontal" | "vertical";
12
+ }
13
+ >({
14
+ size: "default",
15
+ variant: "default",
16
+ spacing: 0,
17
+ orientation: "horizontal",
18
+ });
19
+
20
+ function ToggleGroup({
21
+ className,
22
+ variant,
23
+ size,
24
+ spacing = 0,
25
+ orientation = "horizontal",
26
+ children,
27
+ ...props
28
+ }: ToggleGroupPrimitive.Props &
29
+ VariantProps<typeof toggleVariants> & {
30
+ spacing?: number;
31
+ orientation?: "horizontal" | "vertical";
32
+ }) {
33
+ return (
34
+ <ToggleGroupPrimitive
35
+ data-slot="toggle-group"
36
+ data-variant={variant}
37
+ data-size={size}
38
+ data-spacing={spacing}
39
+ orientation={orientation}
40
+ style={{ "--gap": spacing } as React.CSSProperties}
41
+ className={cn(
42
+ "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch",
43
+ className,
44
+ )}
45
+ {...props}
46
+ >
47
+ <ToggleGroupContext.Provider
48
+ value={{ variant, size, spacing, orientation }}
49
+ >
50
+ {children}
51
+ </ToggleGroupContext.Provider>
52
+ </ToggleGroupPrimitive>
53
+ );
54
+ }
55
+
56
+ function ToggleGroupItem({
57
+ className,
58
+ children,
59
+ variant = "default",
60
+ size = "default",
61
+ ...props
62
+ }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
63
+ const context = React.useContext(ToggleGroupContext);
64
+
65
+ return (
66
+ <TogglePrimitive
67
+ data-slot="toggle-group-item"
68
+ data-variant={context.variant || variant}
69
+ data-size={context.size || size}
70
+ data-spacing={context.spacing}
71
+ className={cn(
72
+ "shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-[orientation=horizontal]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-[orientation=vertical]/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
73
+ toggleVariants({
74
+ variant: context.variant || variant,
75
+ size: context.size || size,
76
+ }),
77
+ className,
78
+ )}
79
+ {...props}
80
+ >
81
+ {children}
82
+ </TogglePrimitive>
83
+ );
84
+ }
85
+
86
+ export { ToggleGroup, ToggleGroupItem };
@@ -0,0 +1,43 @@
1
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
2
+ import { cva, type VariantProps } from "#/lib/cva.ts";
3
+
4
+ import { cn } from "#/lib/utils.ts";
5
+
6
+ const toggleVariants = cva({
7
+ base: "group/toggle inline-flex items-center justify-center gap-1 rounded-lg border border-transparent text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-pressed:bg-muted data-pressed:inset-shadow-xs dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ variants: {
9
+ variant: {
10
+ default: "bg-transparent",
11
+ outline: "border-input bg-transparent hover:bg-muted",
12
+ },
13
+ size: {
14
+ default:
15
+ "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
16
+ sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
17
+ lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ size: "default",
23
+ },
24
+ });
25
+
26
+ type ToggleProps = TogglePrimitive.Props & VariantProps<typeof toggleVariants>;
27
+
28
+ function Toggle({
29
+ className,
30
+ variant = "default",
31
+ size = "default",
32
+ ...props
33
+ }: ToggleProps) {
34
+ return (
35
+ <TogglePrimitive
36
+ data-slot="toggle"
37
+ className={cn(toggleVariants({ variant, size }), className)}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+
43
+ export { Toggle, type ToggleProps, toggleVariants };
@@ -0,0 +1,79 @@
1
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ /**
6
+ * Kit opinion: tooltips open instantly (`delay={0}`) instead of Base UI's
7
+ * default delay. `Tooltip` embeds its own provider, which shadows any outer
8
+ * one - to slow a tooltip down, set `delay` on that `Tooltip` directly.
9
+ */
10
+ function TooltipProvider({
11
+ delay = 0,
12
+ ...props
13
+ }: TooltipPrimitive.Provider.Props) {
14
+ return (
15
+ <TooltipPrimitive.Provider
16
+ data-slot="tooltip-provider"
17
+ delay={delay}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function Tooltip({
24
+ delay,
25
+ ...props
26
+ }: TooltipPrimitive.Root.Props &
27
+ Pick<TooltipPrimitive.Provider.Props, "delay">) {
28
+ return (
29
+ <TooltipProvider delay={delay}>
30
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
31
+ </TooltipProvider>
32
+ );
33
+ }
34
+
35
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
36
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
37
+ }
38
+
39
+ function TooltipContent({
40
+ className,
41
+ side = "top",
42
+ sideOffset = 4,
43
+ align = "center",
44
+ alignOffset = 0,
45
+ collisionPadding,
46
+ children,
47
+ ...props
48
+ }: TooltipPrimitive.Popup.Props &
49
+ Pick<
50
+ TooltipPrimitive.Positioner.Props,
51
+ "align" | "alignOffset" | "collisionPadding" | "side" | "sideOffset"
52
+ >) {
53
+ return (
54
+ <TooltipPrimitive.Portal>
55
+ <TooltipPrimitive.Positioner
56
+ align={align}
57
+ alignOffset={alignOffset}
58
+ collisionPadding={collisionPadding}
59
+ side={side}
60
+ sideOffset={sideOffset}
61
+ className="isolate z-50"
62
+ >
63
+ <TooltipPrimitive.Popup
64
+ data-slot="tooltip-content"
65
+ className={cn(
66
+ "inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:rounded-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 motion-reduce:animate-none!",
67
+ className,
68
+ )}
69
+ {...props}
70
+ >
71
+ {children}
72
+ <TooltipPrimitive.Arrow className="size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
73
+ </TooltipPrimitive.Popup>
74
+ </TooltipPrimitive.Positioner>
75
+ </TooltipPrimitive.Portal>
76
+ );
77
+ }
78
+
79
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };