@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,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
|
+
};
|