@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,44 @@
1
+ import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ type CheckboxGroupProps = CheckboxGroupPrimitive.Props & {
6
+ /**
7
+ * Layout direction of the items. `vertical` stacks them (the default);
8
+ * `horizontal` lays them out in a wrapping row - saves consumers from
9
+ * rebuilding a `flex-row` wrapper for short option sets. (Same API as
10
+ * `RadioGroup`.)
11
+ */
12
+ orientation?: "vertical" | "horizontal";
13
+ };
14
+
15
+ /**
16
+ * Shared `value: string[]` state for a set of `Checkbox`es - each box opts in
17
+ * with its `name` prop. A box given `parent` together with `allValues` on the
18
+ * group becomes the tick-all/indeterminate parent checkbox.
19
+ *
20
+ * In parent mode Base UI overrides every box's `id` for its `aria-controls`
21
+ * wiring, which silently breaks `htmlFor`/`id` label pairs - wrap each box in
22
+ * its `Label` instead (implicit association).
23
+ */
24
+ function CheckboxGroup({
25
+ className,
26
+ orientation = "vertical",
27
+ ...props
28
+ }: CheckboxGroupProps) {
29
+ return (
30
+ <CheckboxGroupPrimitive
31
+ data-slot="checkbox-group"
32
+ data-orientation={orientation}
33
+ className={cn(
34
+ orientation === "horizontal"
35
+ ? "flex flex-wrap items-center gap-x-4 gap-y-2"
36
+ : "grid gap-2",
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ export { CheckboxGroup, type CheckboxGroupProps };
@@ -0,0 +1,34 @@
1
+ import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
2
+ import { CheckIcon, MinusIcon } from "@phosphor-icons/react";
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ type CheckboxProps = CheckboxPrimitive.Root.Props & {
6
+ size?: "sm" | "default";
7
+ };
8
+
9
+ function Checkbox({ className, size = "default", ...props }: CheckboxProps) {
10
+ return (
11
+ <CheckboxPrimitive.Root
12
+ data-slot="checkbox"
13
+ data-size={size}
14
+ className={cn(
15
+ // after:* expands the touch target 12px horizontally and 8px
16
+ // vertically past the visible box; stacked checkboxes need at
17
+ // least that much spacing or adjacent hit areas overlap.
18
+ "peer group/checkbox relative flex shrink-0 items-center justify-center rounded-sm border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:data-checked:border-destructive aria-invalid:data-indeterminate:border-destructive data-[size=default]:size-4 data-[size=sm]:size-3.5 dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground dark:data-indeterminate:bg-primary",
19
+ className,
20
+ )}
21
+ {...props}
22
+ >
23
+ <CheckboxPrimitive.Indicator
24
+ data-slot="checkbox-indicator"
25
+ className="grid animate-in place-content-center text-current duration-150 zoom-in-50 group-data-[size=default]/checkbox:[&>svg]:size-3.5 group-data-[size=sm]/checkbox:[&>svg]:size-3 motion-reduce:animate-none"
26
+ >
27
+ <CheckIcon className="group-data-indeterminate/checkbox:hidden" />
28
+ <MinusIcon className="hidden group-data-indeterminate/checkbox:block" />
29
+ </CheckboxPrimitive.Indicator>
30
+ </CheckboxPrimitive.Root>
31
+ );
32
+ }
33
+
34
+ export { Checkbox, type CheckboxProps };
@@ -0,0 +1,60 @@
1
+ import { XIcon } from "@phosphor-icons/react";
2
+ import type * as React from "react";
3
+ import {
4
+ type BadgeVariants,
5
+ badgeVariants,
6
+ } from "#/components/ui/badge-variants.ts";
7
+
8
+ import { cn } from "#/lib/utils.ts";
9
+
10
+ /**
11
+ * Dismissible tag built on the Badge recipe: same `variant`/`color`/`size`
12
+ * axes, plus a `ChipRemove` button for removable selections (skills catalog,
13
+ * active filters). For a static label, use `Badge` instead.
14
+ */
15
+ function Chip({
16
+ className,
17
+ variant = "secondary",
18
+ color,
19
+ size = "default",
20
+ ...props
21
+ }: React.ComponentProps<"span"> & BadgeVariants) {
22
+ return (
23
+ <span
24
+ data-slot="chip"
25
+ data-variant={variant}
26
+ data-color={color}
27
+ data-size={size}
28
+ className={cn(
29
+ badgeVariants({ variant, color, size }),
30
+ "has-data-[slot=chip-remove]:pr-1",
31
+ size === "sm" && "has-data-[slot=chip-remove]:pr-0.5",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function ChipRemove({
40
+ className,
41
+ children,
42
+ ...props
43
+ }: React.ComponentProps<"button">) {
44
+ return (
45
+ <button
46
+ type="button"
47
+ data-slot="chip-remove"
48
+ aria-label="Remove"
49
+ className={cn(
50
+ "inline-flex size-3.5 shrink-0 cursor-pointer items-center justify-center rounded-full opacity-60 transition-opacity outline-none hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-current/50 [&>svg]:size-2.5",
51
+ className,
52
+ )}
53
+ {...props}
54
+ >
55
+ {children ?? <XIcon aria-hidden />}
56
+ </button>
57
+ );
58
+ }
59
+
60
+ export { Chip, ChipRemove };
@@ -0,0 +1,44 @@
1
+ import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
2
+
3
+ import { cn } from "#/lib/utils.ts";
4
+
5
+ function Collapsible({ className, ...props }: CollapsiblePrimitive.Root.Props) {
6
+ return (
7
+ <CollapsiblePrimitive.Root
8
+ data-slot="collapsible"
9
+ className={cn(className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+
15
+ function CollapsibleTrigger({
16
+ className,
17
+ ...props
18
+ }: CollapsiblePrimitive.Trigger.Props) {
19
+ return (
20
+ <CollapsiblePrimitive.Trigger
21
+ data-slot="collapsible-trigger"
22
+ className={cn(className)}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ function CollapsibleContent({
29
+ className,
30
+ ...props
31
+ }: CollapsiblePrimitive.Panel.Props) {
32
+ return (
33
+ <CollapsiblePrimitive.Panel
34
+ data-slot="collapsible-content"
35
+ className={cn(
36
+ "h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-ending-style:h-0 data-starting-style:h-0 motion-reduce:transition-none",
37
+ className,
38
+ )}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger };
@@ -0,0 +1,191 @@
1
+ import { CaretDownIcon, CheckIcon } from "@phosphor-icons/react";
2
+ import { useRef, useState } from "react";
3
+ import {
4
+ type BadgeColor,
5
+ badgeColorBackgroundClass,
6
+ badgeColorForegroundClass,
7
+ badgeColors,
8
+ } from "#/components/ui/badge-variants.ts";
9
+ import { Button } from "#/components/ui/button.tsx";
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from "#/components/ui/popover.tsx";
15
+ import { cn } from "#/lib/utils.ts";
16
+
17
+ const GRID_COLUMNS = 5;
18
+
19
+ /** Human-friendly label for a palette name: "blue" → "Blue". */
20
+ const formatColorName = (name: string): string =>
21
+ name.charAt(0).toUpperCase() + name.slice(1);
22
+
23
+ const ARROW_KEY_OFFSETS: Record<string, number> = {
24
+ ArrowLeft: -1,
25
+ ArrowRight: 1,
26
+ ArrowUp: -GRID_COLUMNS,
27
+ ArrowDown: GRID_COLUMNS,
28
+ };
29
+
30
+ /**
31
+ * Select-like trigger opening a swatch radiogroup over the catalog palette.
32
+ * Swatch classes come from the static maps in badge-variants.ts so the palette never
33
+ * relies on runtime class composition.
34
+ */
35
+ function ColorPicker({
36
+ value: controlledValue,
37
+ defaultValue = null,
38
+ onValueChange,
39
+ placeholder = "Select a color",
40
+ clearLabel = "Clear selection",
41
+ clearable = false,
42
+ name,
43
+ disabled = false,
44
+ className,
45
+ contentClassName,
46
+ }: {
47
+ /** Controlled selection; omit to let the picker manage its own state. */
48
+ value?: BadgeColor | null;
49
+ /** Initial selection for uncontrolled usage. */
50
+ defaultValue?: BadgeColor | null;
51
+ onValueChange?: (color: BadgeColor | null) => void;
52
+ placeholder?: string;
53
+ clearLabel?: string;
54
+ /** Offer a clear affordance that reports `null`. */
55
+ clearable?: boolean;
56
+ /** When set, renders a hidden input so plain form posts include the color. */
57
+ name?: string;
58
+ disabled?: boolean;
59
+ className?: string;
60
+ contentClassName?: string;
61
+ }) {
62
+ const [open, setOpen] = useState(false);
63
+ const [uncontrolledValue, setUncontrolledValue] = useState<BadgeColor | null>(
64
+ defaultValue,
65
+ );
66
+ const gridRef = useRef<HTMLDivElement>(null);
67
+
68
+ const isControlled = controlledValue !== undefined;
69
+ const value = isControlled ? controlledValue : uncontrolledValue;
70
+
71
+ // Roving tabindex: a radiogroup exposes a single tab stop — the selected
72
+ // swatch, or the first one while nothing is selected.
73
+ const tabbableColor = value ?? badgeColors[0];
74
+
75
+ const selectColor = (next: BadgeColor | null) => {
76
+ if (!isControlled) setUncontrolledValue(next);
77
+ onValueChange?.(next);
78
+ setOpen(false);
79
+ };
80
+
81
+ const handleGridKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
82
+ const offset = ARROW_KEY_OFFSETS[event.key];
83
+ if (offset === undefined) return;
84
+ const swatches = gridRef.current?.querySelectorAll<HTMLButtonElement>(
85
+ "[data-slot=color-picker-swatch]",
86
+ );
87
+ if (swatches === undefined || swatches.length === 0) return;
88
+ const currentIndex = Array.prototype.indexOf.call(
89
+ swatches,
90
+ document.activeElement,
91
+ );
92
+ if (currentIndex === -1) return;
93
+ event.preventDefault();
94
+ const nextIndex = Math.min(
95
+ Math.max(currentIndex + offset, 0),
96
+ swatches.length - 1,
97
+ );
98
+ swatches[nextIndex]?.focus();
99
+ };
100
+
101
+ return (
102
+ <Popover open={open} onOpenChange={setOpen}>
103
+ <PopoverTrigger
104
+ render={(props) => (
105
+ <Button
106
+ {...props}
107
+ type="button"
108
+ variant="outline"
109
+ disabled={disabled}
110
+ data-slot="color-picker-trigger"
111
+ className={cn(
112
+ "w-full justify-between gap-2 font-normal",
113
+ className,
114
+ )}
115
+ >
116
+ <span className="flex min-w-0 items-center gap-2">
117
+ {value ? (
118
+ <span
119
+ aria-hidden
120
+ className={cn(
121
+ "size-4 shrink-0 rounded-full",
122
+ badgeColorBackgroundClass[value],
123
+ )}
124
+ />
125
+ ) : null}
126
+ <span
127
+ className={cn("truncate", !value && "text-muted-foreground")}
128
+ >
129
+ {value ? formatColorName(value) : placeholder}
130
+ </span>
131
+ </span>
132
+ <CaretDownIcon className="size-4 shrink-0 text-muted-foreground" />
133
+ </Button>
134
+ )}
135
+ />
136
+ {name !== undefined && (
137
+ <input type="hidden" name={name} value={value ?? ""} />
138
+ )}
139
+ <PopoverContent align="start" className={cn("w-auto", contentClassName)}>
140
+ <div
141
+ ref={gridRef}
142
+ role="radiogroup"
143
+ aria-label={placeholder}
144
+ data-slot="color-picker-grid"
145
+ className="grid grid-cols-5 gap-1.5 p-0.5"
146
+ onKeyDown={handleGridKeyDown}
147
+ >
148
+ {badgeColors.map((color) => (
149
+ <button
150
+ key={color}
151
+ type="button"
152
+ role="radio"
153
+ aria-checked={value === color}
154
+ tabIndex={color === tabbableColor ? 0 : -1}
155
+ data-slot="color-picker-swatch"
156
+ title={formatColorName(color)}
157
+ aria-label={formatColorName(color)}
158
+ onClick={() => selectColor(color)}
159
+ className={cn(
160
+ "flex size-6 items-center justify-center rounded-full ring-ring ring-offset-2 ring-offset-popover outline-none transition-transform hover:scale-110 focus-visible:ring-2 motion-reduce:transform-none",
161
+ badgeColorBackgroundClass[color],
162
+ value === color && "ring-2",
163
+ )}
164
+ >
165
+ {value === color ? (
166
+ <CheckIcon
167
+ aria-hidden
168
+ weight="bold"
169
+ className={cn("size-3.5", badgeColorForegroundClass[color])}
170
+ />
171
+ ) : null}
172
+ </button>
173
+ ))}
174
+ </div>
175
+ {clearable && value != null && (
176
+ <Button
177
+ type="button"
178
+ variant="ghost"
179
+ size="sm"
180
+ data-slot="color-picker-clear"
181
+ onClick={() => selectColor(null)}
182
+ >
183
+ {clearLabel}
184
+ </Button>
185
+ )}
186
+ </PopoverContent>
187
+ </Popover>
188
+ );
189
+ }
190
+
191
+ export { ColorPicker };
@@ -0,0 +1,317 @@
1
+ import { Combobox as ComboboxPrimitive } from "@base-ui/react";
2
+ import { CaretDownIcon, CheckIcon, XIcon } from "@phosphor-icons/react";
3
+ import * as React from "react";
4
+ import { Button } from "#/components/ui/button.tsx";
5
+ import {
6
+ InputGroup,
7
+ InputGroupAddon,
8
+ InputGroupButton,
9
+ InputGroupInput,
10
+ } from "#/components/ui/input-group.tsx";
11
+ import {
12
+ menuContentVariants,
13
+ menuIndicatorVariants,
14
+ menuItemVariants,
15
+ menuLabelVariants,
16
+ menuSeparatorVariants,
17
+ } from "#/components/ui/menu-variants.ts";
18
+ import { cn } from "#/lib/utils.ts";
19
+
20
+ // Base UI's combobox root renders no DOM element of its own, so it can't carry
21
+ // a `data-slot` the way the other kit roots do; we still wrap it in a named
22
+ // generic function for API consistency (and a discoverable doc anchor) while
23
+ // forwarding the value/multiple generics intact.
24
+ function Combobox<Value, Multiple extends boolean | undefined = false>(
25
+ props: ComboboxPrimitive.Root.Props<Value, Multiple>,
26
+ ) {
27
+ return <ComboboxPrimitive.Root {...props} />;
28
+ }
29
+
30
+ function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
31
+ return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
32
+ }
33
+
34
+ function ComboboxTrigger({
35
+ className,
36
+ children,
37
+ "aria-label": ariaLabel = "Open list",
38
+ ...props
39
+ }: ComboboxPrimitive.Trigger.Props) {
40
+ return (
41
+ <ComboboxPrimitive.Trigger
42
+ data-slot="combobox-trigger"
43
+ aria-label={ariaLabel}
44
+ className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
45
+ {...props}
46
+ >
47
+ {children}
48
+ <CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />
49
+ </ComboboxPrimitive.Trigger>
50
+ );
51
+ }
52
+
53
+ function ComboboxClear({
54
+ "aria-label": ariaLabel = "Clear selection",
55
+ ...props
56
+ }: ComboboxPrimitive.Clear.Props) {
57
+ return (
58
+ <ComboboxPrimitive.Clear
59
+ data-slot="combobox-clear"
60
+ aria-label={ariaLabel}
61
+ render={<InputGroupButton variant="ghost" size="icon-xs" />}
62
+ {...props}
63
+ >
64
+ <XIcon className="pointer-events-none" />
65
+ </ComboboxPrimitive.Clear>
66
+ );
67
+ }
68
+
69
+ function ComboboxInput({
70
+ className,
71
+ children,
72
+ disabled = false,
73
+ showTrigger = true,
74
+ showClear = false,
75
+ ...props
76
+ }: ComboboxPrimitive.Input.Props & {
77
+ showTrigger?: boolean;
78
+ showClear?: boolean;
79
+ }) {
80
+ return (
81
+ <InputGroup className={cn("w-auto", className)}>
82
+ <ComboboxPrimitive.Input
83
+ render={<InputGroupInput disabled={disabled} />}
84
+ {...props}
85
+ />
86
+ <InputGroupAddon align="inline-end">
87
+ {showTrigger && (
88
+ <InputGroupButton
89
+ size="icon-xs"
90
+ variant="ghost"
91
+ render={<ComboboxTrigger />}
92
+ data-slot="input-group-button"
93
+ className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
94
+ disabled={disabled}
95
+ />
96
+ )}
97
+ {showClear && <ComboboxClear disabled={disabled} />}
98
+ </InputGroupAddon>
99
+ {children}
100
+ </InputGroup>
101
+ );
102
+ }
103
+
104
+ function ComboboxContent({
105
+ className,
106
+ side = "bottom",
107
+ sideOffset = 6,
108
+ align = "start",
109
+ alignOffset = 0,
110
+ anchor,
111
+ ...props
112
+ }: ComboboxPrimitive.Popup.Props &
113
+ Pick<
114
+ ComboboxPrimitive.Positioner.Props,
115
+ "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
116
+ >) {
117
+ return (
118
+ <ComboboxPrimitive.Portal>
119
+ <ComboboxPrimitive.Positioner
120
+ side={side}
121
+ sideOffset={sideOffset}
122
+ align={align}
123
+ alignOffset={alignOffset}
124
+ anchor={anchor}
125
+ className="isolate z-50"
126
+ >
127
+ <ComboboxPrimitive.Popup
128
+ data-slot="combobox-content"
129
+ data-chips={!!anchor}
130
+ className={cn(
131
+ menuContentVariants(),
132
+ "group/combobox-content relative w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] overflow-hidden p-0 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none",
133
+ className,
134
+ )}
135
+ {...props}
136
+ />
137
+ </ComboboxPrimitive.Positioner>
138
+ </ComboboxPrimitive.Portal>
139
+ );
140
+ }
141
+
142
+ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
143
+ return (
144
+ <ComboboxPrimitive.List
145
+ data-slot="combobox-list"
146
+ // max-height caps the list at 72 spacing units (18rem) or the available
147
+ // popup height, whichever is smaller, minus 9 units (2.25rem) reserved
148
+ // for an in-popup input group (command-style search) when present.
149
+ className={cn(
150
+ "no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
151
+ className,
152
+ )}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ function ComboboxItem({
159
+ className,
160
+ children,
161
+ ...props
162
+ }: ComboboxPrimitive.Item.Props) {
163
+ return (
164
+ <ComboboxPrimitive.Item
165
+ data-slot="combobox-item"
166
+ className={cn(
167
+ menuItemVariants({ indicator: "end" }),
168
+ "w-full",
169
+ className,
170
+ )}
171
+ {...props}
172
+ >
173
+ {children}
174
+ <ComboboxPrimitive.ItemIndicator
175
+ render={<span className={menuIndicatorVariants()} />}
176
+ >
177
+ <CheckIcon className="pointer-events-none" />
178
+ </ComboboxPrimitive.ItemIndicator>
179
+ </ComboboxPrimitive.Item>
180
+ );
181
+ }
182
+
183
+ function ComboboxRow({ ...props }: ComboboxPrimitive.Row.Props) {
184
+ return <ComboboxPrimitive.Row data-slot="combobox-row" {...props} />;
185
+ }
186
+
187
+ function ComboboxGroup({ ...props }: ComboboxPrimitive.Group.Props) {
188
+ return <ComboboxPrimitive.Group data-slot="combobox-group" {...props} />;
189
+ }
190
+
191
+ function ComboboxLabel({
192
+ className,
193
+ ...props
194
+ }: ComboboxPrimitive.GroupLabel.Props) {
195
+ return (
196
+ <ComboboxPrimitive.GroupLabel
197
+ data-slot="combobox-label"
198
+ className={cn(menuLabelVariants(), className)}
199
+ {...props}
200
+ />
201
+ );
202
+ }
203
+
204
+ function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
205
+ return (
206
+ <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
207
+ );
208
+ }
209
+
210
+ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
211
+ return (
212
+ <ComboboxPrimitive.Empty
213
+ data-slot="combobox-empty"
214
+ className={cn(
215
+ "hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
216
+ className,
217
+ )}
218
+ {...props}
219
+ />
220
+ );
221
+ }
222
+
223
+ function ComboboxSeparator({
224
+ className,
225
+ ...props
226
+ }: ComboboxPrimitive.Separator.Props) {
227
+ return (
228
+ <ComboboxPrimitive.Separator
229
+ data-slot="combobox-separator"
230
+ className={cn(menuSeparatorVariants(), className)}
231
+ {...props}
232
+ />
233
+ );
234
+ }
235
+
236
+ function ComboboxChips({ className, ...props }: ComboboxPrimitive.Chips.Props) {
237
+ return (
238
+ <ComboboxPrimitive.Chips
239
+ data-slot="combobox-chips"
240
+ className={cn(
241
+ "flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
242
+ className,
243
+ )}
244
+ {...props}
245
+ />
246
+ );
247
+ }
248
+
249
+ function ComboboxChip({
250
+ className,
251
+ children,
252
+ showRemove = true,
253
+ ...props
254
+ }: ComboboxPrimitive.Chip.Props & {
255
+ showRemove?: boolean;
256
+ }) {
257
+ return (
258
+ <ComboboxPrimitive.Chip
259
+ data-slot="combobox-chip"
260
+ // h-5.25 (21px) keeps chips compact enough to sit two-deep inside the
261
+ // 32px-min chips row without forcing it to grow.
262
+ className={cn(
263
+ "flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
264
+ className,
265
+ )}
266
+ {...props}
267
+ >
268
+ {children}
269
+ {showRemove && (
270
+ <ComboboxPrimitive.ChipRemove
271
+ render={<Button variant="ghost" size="icon-xs" />}
272
+ className="-ml-1 opacity-50 hover:opacity-100"
273
+ data-slot="combobox-chip-remove"
274
+ >
275
+ <XIcon className="pointer-events-none" />
276
+ </ComboboxPrimitive.ChipRemove>
277
+ )}
278
+ </ComboboxPrimitive.Chip>
279
+ );
280
+ }
281
+
282
+ function ComboboxChipsInput({
283
+ className,
284
+ ...props
285
+ }: ComboboxPrimitive.Input.Props) {
286
+ return (
287
+ <ComboboxPrimitive.Input
288
+ data-slot="combobox-chip-input"
289
+ className={cn("min-w-16 flex-1 outline-none", className)}
290
+ {...props}
291
+ />
292
+ );
293
+ }
294
+
295
+ function useComboboxAnchor() {
296
+ return React.useRef<HTMLDivElement | null>(null);
297
+ }
298
+
299
+ export {
300
+ Combobox,
301
+ ComboboxChip,
302
+ ComboboxChips,
303
+ ComboboxChipsInput,
304
+ ComboboxCollection,
305
+ ComboboxContent,
306
+ ComboboxEmpty,
307
+ ComboboxGroup,
308
+ ComboboxInput,
309
+ ComboboxItem,
310
+ ComboboxLabel,
311
+ ComboboxList,
312
+ ComboboxRow,
313
+ ComboboxSeparator,
314
+ ComboboxTrigger,
315
+ ComboboxValue,
316
+ useComboboxAnchor,
317
+ };