@cntyclub/ui-react 0.1.0
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/dist/chunk-HDGMSYQS.js +26461 -0
- package/dist/chunk-HDGMSYQS.js.map +1 -0
- package/dist/chunk-PR4QN5HX.js +39 -0
- package/dist/chunk-PR4QN5HX.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +5207 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +1462 -0
- package/dist/index.js +81862 -0
- package/dist/index.js.map +1 -0
- package/dist/input-CZvh825j.d.ts +24 -0
- package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
- package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
- package/package.json +79 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +95 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +744 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +143 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +229 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +125 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +300 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
|
4
|
+
import {
|
|
5
|
+
Menubar as MenubarPrimitive,
|
|
6
|
+
type MenubarProps,
|
|
7
|
+
} from "@base-ui/react/menubar";
|
|
8
|
+
import type * as React from "react";
|
|
9
|
+
|
|
10
|
+
import { cn } from "../../lib/utils/css";
|
|
11
|
+
import {
|
|
12
|
+
Menu,
|
|
13
|
+
MenuCheckboxItem,
|
|
14
|
+
MenuGroup,
|
|
15
|
+
MenuGroupLabel,
|
|
16
|
+
MenuItem,
|
|
17
|
+
MenuPopup,
|
|
18
|
+
MenuPortal,
|
|
19
|
+
MenuRadioGroup,
|
|
20
|
+
MenuRadioItem,
|
|
21
|
+
MenuSeparator,
|
|
22
|
+
MenuShortcut,
|
|
23
|
+
MenuSub,
|
|
24
|
+
MenuSubPopup,
|
|
25
|
+
MenuSubTrigger,
|
|
26
|
+
} from "./menu";
|
|
27
|
+
|
|
28
|
+
function Menubar({ className, ...props }: MenubarProps) {
|
|
29
|
+
return (
|
|
30
|
+
<MenubarPrimitive
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex h-9 w-fit items-center gap-1 rounded-lg border bg-background p-1 shadow-xs/5 sm:h-8 dark:bg-input/32",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
data-slot="menubar"
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function MenubarMenu(props: MenuPrimitive.Root.Props) {
|
|
42
|
+
return <Menu data-slot="menubar-menu" {...props} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function MenubarTrigger({
|
|
46
|
+
className,
|
|
47
|
+
...props
|
|
48
|
+
}: MenuPrimitive.Trigger.Props) {
|
|
49
|
+
return (
|
|
50
|
+
<MenuPrimitive.Trigger
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex select-none items-center rounded-md px-2 py-1 font-medium text-base outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-popup-open:bg-accent data-popup-open:text-accent-foreground sm:text-sm",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
data-slot="menubar-trigger"
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function MenubarContent({
|
|
62
|
+
align = "start",
|
|
63
|
+
sideOffset = 8,
|
|
64
|
+
...props
|
|
65
|
+
}: React.ComponentProps<typeof MenuPopup>) {
|
|
66
|
+
return (
|
|
67
|
+
<MenuPopup
|
|
68
|
+
align={align}
|
|
69
|
+
data-slot="menubar-content"
|
|
70
|
+
sideOffset={sideOffset}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
Menubar,
|
|
78
|
+
MenubarMenu,
|
|
79
|
+
MenubarTrigger,
|
|
80
|
+
MenubarContent,
|
|
81
|
+
MenuPortal as MenubarPortal,
|
|
82
|
+
MenuGroup as MenubarGroup,
|
|
83
|
+
MenuItem as MenubarItem,
|
|
84
|
+
MenuCheckboxItem as MenubarCheckboxItem,
|
|
85
|
+
MenuRadioGroup as MenubarRadioGroup,
|
|
86
|
+
MenuRadioItem as MenubarRadioItem,
|
|
87
|
+
MenuGroupLabel as MenubarLabel,
|
|
88
|
+
MenuSeparator as MenubarSeparator,
|
|
89
|
+
MenuShortcut as MenubarShortcut,
|
|
90
|
+
MenuSub as MenubarSub,
|
|
91
|
+
MenuSubTrigger as MenubarSubTrigger,
|
|
92
|
+
MenuSubPopup as MenubarSubContent,
|
|
93
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Meter as MeterPrimitive } from "@base-ui/react/meter";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils/css";
|
|
6
|
+
|
|
7
|
+
function Meter({ className, children, ...props }: MeterPrimitive.Root.Props) {
|
|
8
|
+
return (
|
|
9
|
+
<MeterPrimitive.Root
|
|
10
|
+
className={cn("flex w-full flex-col gap-2", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
>
|
|
13
|
+
{children ? (
|
|
14
|
+
children
|
|
15
|
+
) : (
|
|
16
|
+
<MeterTrack>
|
|
17
|
+
<MeterIndicator />
|
|
18
|
+
</MeterTrack>
|
|
19
|
+
)}
|
|
20
|
+
</MeterPrimitive.Root>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function MeterLabel({ className, ...props }: MeterPrimitive.Label.Props) {
|
|
25
|
+
return (
|
|
26
|
+
<MeterPrimitive.Label
|
|
27
|
+
className={cn("font-medium text-foreground text-sm", className)}
|
|
28
|
+
data-slot="meter-label"
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function MeterTrack({ className, ...props }: MeterPrimitive.Track.Props) {
|
|
35
|
+
return (
|
|
36
|
+
<MeterPrimitive.Track
|
|
37
|
+
className={cn("block h-2 w-full overflow-hidden bg-input", className)}
|
|
38
|
+
data-slot="meter-track"
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function MeterIndicator({
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: MeterPrimitive.Indicator.Props) {
|
|
48
|
+
return (
|
|
49
|
+
<MeterPrimitive.Indicator
|
|
50
|
+
className={cn("bg-primary transition-all duration-500", className)}
|
|
51
|
+
data-slot="meter-indicator"
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function MeterValue({ className, ...props }: MeterPrimitive.Value.Props) {
|
|
58
|
+
return (
|
|
59
|
+
<MeterPrimitive.Value
|
|
60
|
+
className={cn("text-foreground text-sm tabular-nums", className)}
|
|
61
|
+
data-slot="meter-value"
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Meter, MeterLabel, MeterTrack, MeterIndicator, MeterValue };
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Select as SelectPrimitive } from "@base-ui/react/select";
|
|
4
|
+
import { ChevronsUpDownIcon, SearchIcon } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils/css";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
|
|
10
|
+
interface MultiSelectItem {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface MultiSelectProps {
|
|
17
|
+
items: MultiSelectItem[];
|
|
18
|
+
/** Controlled selected values. */
|
|
19
|
+
value?: string[];
|
|
20
|
+
/** Uncontrolled initial selected values. */
|
|
21
|
+
defaultValue?: string[];
|
|
22
|
+
onValueChange?: (value: string[]) => void;
|
|
23
|
+
size?: "sm" | "default" | "lg";
|
|
24
|
+
placeholder?: React.ReactNode;
|
|
25
|
+
/** Extra context rendered next to the selected count, e.g. "of 12 users". */
|
|
26
|
+
supportingText?: React.ReactNode;
|
|
27
|
+
/** Formats the trigger text from the selected count. Defaults to `${count} selected`. */
|
|
28
|
+
selectedCountFormatter?: (count: number) => React.ReactNode;
|
|
29
|
+
showSearch?: boolean;
|
|
30
|
+
searchPlaceholder?: string;
|
|
31
|
+
showFooter?: boolean;
|
|
32
|
+
selectAllLabel?: React.ReactNode;
|
|
33
|
+
resetLabel?: React.ReactNode;
|
|
34
|
+
emptyStateTitle?: React.ReactNode;
|
|
35
|
+
emptyStateDescription?: React.ReactNode;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
name?: string;
|
|
38
|
+
required?: boolean;
|
|
39
|
+
id?: string;
|
|
40
|
+
/** Trigger class name. */
|
|
41
|
+
className?: string;
|
|
42
|
+
popupClassName?: string;
|
|
43
|
+
sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function MultiSelect({
|
|
47
|
+
items,
|
|
48
|
+
value: valueProp,
|
|
49
|
+
defaultValue,
|
|
50
|
+
onValueChange,
|
|
51
|
+
size = "default",
|
|
52
|
+
placeholder = "Select options",
|
|
53
|
+
supportingText,
|
|
54
|
+
selectedCountFormatter = (count) => `${count} selected`,
|
|
55
|
+
showSearch = true,
|
|
56
|
+
searchPlaceholder = "Search",
|
|
57
|
+
showFooter = true,
|
|
58
|
+
selectAllLabel = "Select all",
|
|
59
|
+
resetLabel = "Reset",
|
|
60
|
+
emptyStateTitle = "No results found",
|
|
61
|
+
emptyStateDescription,
|
|
62
|
+
disabled,
|
|
63
|
+
name,
|
|
64
|
+
required,
|
|
65
|
+
id,
|
|
66
|
+
className,
|
|
67
|
+
popupClassName,
|
|
68
|
+
sideOffset = 4,
|
|
69
|
+
}: MultiSelectProps) {
|
|
70
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState<string[]>(
|
|
71
|
+
defaultValue ?? [],
|
|
72
|
+
);
|
|
73
|
+
const [search, setSearch] = React.useState("");
|
|
74
|
+
|
|
75
|
+
const isControlled = valueProp !== undefined;
|
|
76
|
+
const value = isControlled ? valueProp : uncontrolledValue;
|
|
77
|
+
|
|
78
|
+
const setValue = (next: string[]) => {
|
|
79
|
+
if (!isControlled) {
|
|
80
|
+
setUncontrolledValue(next);
|
|
81
|
+
}
|
|
82
|
+
onValueChange?.(next);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const query = search.trim().toLowerCase();
|
|
86
|
+
const filteredItems = query
|
|
87
|
+
? items.filter((item) => item.label.toLowerCase().includes(query))
|
|
88
|
+
: items;
|
|
89
|
+
|
|
90
|
+
const count = value.length;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<SelectPrimitive.Root
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
multiple
|
|
96
|
+
name={name}
|
|
97
|
+
onOpenChange={(open) => {
|
|
98
|
+
if (!open) {
|
|
99
|
+
setSearch("");
|
|
100
|
+
}
|
|
101
|
+
}}
|
|
102
|
+
onValueChange={(next) => setValue(next as string[])}
|
|
103
|
+
required={required}
|
|
104
|
+
value={value}
|
|
105
|
+
>
|
|
106
|
+
<SelectPrimitive.Trigger
|
|
107
|
+
className={cn(
|
|
108
|
+
"relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-center gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base text-foreground shadow-xs/5 outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
|
|
109
|
+
size === "sm" &&
|
|
110
|
+
"min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
|
|
111
|
+
size === "lg" && "min-h-10 sm:min-h-9",
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
data-slot="multi-select-trigger"
|
|
115
|
+
id={id}
|
|
116
|
+
>
|
|
117
|
+
<span className="flex flex-1 items-baseline gap-1.5 truncate">
|
|
118
|
+
{count > 0 ? (
|
|
119
|
+
<>
|
|
120
|
+
<span className="truncate">{selectedCountFormatter(count)}</span>
|
|
121
|
+
{supportingText ? (
|
|
122
|
+
<span className="truncate font-normal text-muted-foreground">
|
|
123
|
+
{supportingText}
|
|
124
|
+
</span>
|
|
125
|
+
) : null}
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
128
|
+
<span className="truncate text-muted-foreground">
|
|
129
|
+
{placeholder}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</span>
|
|
133
|
+
<SelectPrimitive.Icon data-slot="multi-select-icon">
|
|
134
|
+
<ChevronsUpDownIcon className="-me-1 size-4.5 opacity-80 sm:size-4" />
|
|
135
|
+
</SelectPrimitive.Icon>
|
|
136
|
+
</SelectPrimitive.Trigger>
|
|
137
|
+
<SelectPrimitive.Portal>
|
|
138
|
+
<SelectPrimitive.Positioner
|
|
139
|
+
alignItemWithTrigger={false}
|
|
140
|
+
className="z-50 select-none"
|
|
141
|
+
data-slot="multi-select-positioner"
|
|
142
|
+
sideOffset={sideOffset}
|
|
143
|
+
>
|
|
144
|
+
<SelectPrimitive.Popup
|
|
145
|
+
className={cn(
|
|
146
|
+
"relative flex max-h-[min(var(--available-height),20rem)] w-(--anchor-width) min-w-48 max-w-(--available-width) origin-(--transform-origin) flex-col overflow-hidden rounded-lg border bg-popover not-dark:bg-clip-padding text-foreground shadow-lg/5 transition-[transform,scale,opacity] duration-150 ease-out before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
|
|
147
|
+
popupClassName,
|
|
148
|
+
)}
|
|
149
|
+
data-slot="multi-select-popup"
|
|
150
|
+
>
|
|
151
|
+
{showSearch ? (
|
|
152
|
+
<MultiSelectSearch
|
|
153
|
+
onChange={setSearch}
|
|
154
|
+
placeholder={searchPlaceholder}
|
|
155
|
+
value={search}
|
|
156
|
+
/>
|
|
157
|
+
) : null}
|
|
158
|
+
<SelectPrimitive.List
|
|
159
|
+
className="min-h-0 flex-1 overflow-y-auto p-1"
|
|
160
|
+
data-slot="multi-select-list"
|
|
161
|
+
>
|
|
162
|
+
{filteredItems.map((item) => (
|
|
163
|
+
<MultiSelectOption
|
|
164
|
+
disabled={item.disabled}
|
|
165
|
+
key={item.value}
|
|
166
|
+
label={item.label}
|
|
167
|
+
value={item.value}
|
|
168
|
+
/>
|
|
169
|
+
))}
|
|
170
|
+
{filteredItems.length === 0 ? (
|
|
171
|
+
<div
|
|
172
|
+
className="flex flex-col items-center justify-center gap-0.5 px-4 py-6 text-center"
|
|
173
|
+
data-slot="multi-select-empty"
|
|
174
|
+
>
|
|
175
|
+
<p className="font-medium text-foreground text-sm">
|
|
176
|
+
{emptyStateTitle}
|
|
177
|
+
</p>
|
|
178
|
+
{emptyStateDescription ? (
|
|
179
|
+
<p className="text-muted-foreground text-sm">
|
|
180
|
+
{emptyStateDescription}
|
|
181
|
+
</p>
|
|
182
|
+
) : null}
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
185
|
+
</SelectPrimitive.List>
|
|
186
|
+
{showFooter ? (
|
|
187
|
+
<div
|
|
188
|
+
className="flex items-center justify-between gap-1 border-t p-1"
|
|
189
|
+
data-slot="multi-select-footer"
|
|
190
|
+
>
|
|
191
|
+
<Button
|
|
192
|
+
onClick={() =>
|
|
193
|
+
setValue(
|
|
194
|
+
items
|
|
195
|
+
.filter((item) => !item.disabled)
|
|
196
|
+
.map((item) => item.value),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
size="xs"
|
|
200
|
+
variant="ghost"
|
|
201
|
+
>
|
|
202
|
+
{selectAllLabel}
|
|
203
|
+
</Button>
|
|
204
|
+
<Button
|
|
205
|
+
className="text-muted-foreground"
|
|
206
|
+
onClick={() => setValue([])}
|
|
207
|
+
size="xs"
|
|
208
|
+
variant="ghost"
|
|
209
|
+
>
|
|
210
|
+
{resetLabel}
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
) : null}
|
|
214
|
+
</SelectPrimitive.Popup>
|
|
215
|
+
</SelectPrimitive.Positioner>
|
|
216
|
+
</SelectPrimitive.Portal>
|
|
217
|
+
</SelectPrimitive.Root>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function MultiSelectSearch({
|
|
222
|
+
value,
|
|
223
|
+
onChange,
|
|
224
|
+
placeholder,
|
|
225
|
+
}: {
|
|
226
|
+
value: string;
|
|
227
|
+
onChange: (value: string) => void;
|
|
228
|
+
placeholder?: string;
|
|
229
|
+
}) {
|
|
230
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
231
|
+
|
|
232
|
+
// Focus after Base UI's own initial focus management has run.
|
|
233
|
+
React.useEffect(() => {
|
|
234
|
+
const frame = requestAnimationFrame(() => inputRef.current?.focus());
|
|
235
|
+
return () => cancelAnimationFrame(frame);
|
|
236
|
+
}, []);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
className="flex shrink-0 items-center gap-2 border-b px-2.5"
|
|
241
|
+
data-slot="multi-select-search"
|
|
242
|
+
>
|
|
243
|
+
<SearchIcon className="size-4.5 shrink-0 opacity-64 sm:size-4" />
|
|
244
|
+
<input
|
|
245
|
+
className="h-9 min-w-0 flex-1 bg-transparent text-base text-foreground outline-none placeholder:text-muted-foreground sm:h-8 sm:text-sm"
|
|
246
|
+
onChange={(event) => onChange(event.target.value)}
|
|
247
|
+
onKeyDown={(event) => {
|
|
248
|
+
// Let list navigation keys bubble to Base UI; keep everything else
|
|
249
|
+
// (printable characters, Space, Backspace…) from triggering the
|
|
250
|
+
// select's typeahead while typing in the search box.
|
|
251
|
+
if (
|
|
252
|
+
event.key !== "ArrowDown" &&
|
|
253
|
+
event.key !== "ArrowUp" &&
|
|
254
|
+
event.key !== "Enter" &&
|
|
255
|
+
event.key !== "Escape"
|
|
256
|
+
) {
|
|
257
|
+
event.stopPropagation();
|
|
258
|
+
}
|
|
259
|
+
}}
|
|
260
|
+
placeholder={placeholder}
|
|
261
|
+
ref={inputRef}
|
|
262
|
+
value={value}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function MultiSelectOption({
|
|
269
|
+
label,
|
|
270
|
+
value,
|
|
271
|
+
disabled,
|
|
272
|
+
}: {
|
|
273
|
+
label: string;
|
|
274
|
+
value: string;
|
|
275
|
+
disabled?: boolean;
|
|
276
|
+
}) {
|
|
277
|
+
return (
|
|
278
|
+
<SelectPrimitive.Item
|
|
279
|
+
className="grid min-h-8 cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
|
280
|
+
data-slot="multi-select-item"
|
|
281
|
+
disabled={disabled}
|
|
282
|
+
value={value}
|
|
283
|
+
>
|
|
284
|
+
<SelectPrimitive.ItemIndicator className="col-start-1">
|
|
285
|
+
{/** biome-ignore lint/a11y/noSvgWithoutTitle: Imported from library */}
|
|
286
|
+
<svg
|
|
287
|
+
fill="none"
|
|
288
|
+
height="24"
|
|
289
|
+
stroke="currentColor"
|
|
290
|
+
strokeLinecap="round"
|
|
291
|
+
strokeLinejoin="round"
|
|
292
|
+
strokeWidth="2"
|
|
293
|
+
viewBox="0 0 24 24"
|
|
294
|
+
width="24"
|
|
295
|
+
xmlns="http://www.w3.org/1500/svg"
|
|
296
|
+
>
|
|
297
|
+
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
|
|
298
|
+
</svg>
|
|
299
|
+
</SelectPrimitive.ItemIndicator>
|
|
300
|
+
<SelectPrimitive.ItemText className="col-start-2 min-w-0 truncate">
|
|
301
|
+
{label}
|
|
302
|
+
</SelectPrimitive.ItemText>
|
|
303
|
+
</SelectPrimitive.Item>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export { MultiSelect };
|
|
308
|
+
export type { MultiSelectItem, MultiSelectProps };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../lib/utils/css";
|
|
9
|
+
|
|
10
|
+
function NavigationMenu({
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
...props
|
|
14
|
+
}: NavigationMenuPrimitive.Root.Props) {
|
|
15
|
+
return (
|
|
16
|
+
<NavigationMenuPrimitive.Root
|
|
17
|
+
className={cn(
|
|
18
|
+
"relative flex max-w-max flex-1 items-center justify-center",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
data-slot="navigation-menu"
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
<NavigationMenuPrimitive.Portal>
|
|
26
|
+
<NavigationMenuPrimitive.Positioner
|
|
27
|
+
className="z-50 box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] before:absolute before:content-['']"
|
|
28
|
+
collisionAvoidance={{ side: "none" }}
|
|
29
|
+
collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
|
|
30
|
+
data-slot="navigation-menu-positioner"
|
|
31
|
+
sideOffset={8}
|
|
32
|
+
>
|
|
33
|
+
<NavigationMenuPrimitive.Popup
|
|
34
|
+
className="relative h-(--popup-height) w-full min-w-32 origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 outline-none transition-[opacity,transform,width,height,scale,translate] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 sm:w-(--popup-width) dark:before:shadow-[0_-1px_--theme(--color-white/6%)]"
|
|
35
|
+
data-slot="navigation-menu-popup"
|
|
36
|
+
>
|
|
37
|
+
<NavigationMenuPrimitive.Viewport
|
|
38
|
+
className="relative h-full w-full overflow-hidden"
|
|
39
|
+
data-slot="navigation-menu-viewport"
|
|
40
|
+
/>
|
|
41
|
+
</NavigationMenuPrimitive.Popup>
|
|
42
|
+
</NavigationMenuPrimitive.Positioner>
|
|
43
|
+
</NavigationMenuPrimitive.Portal>
|
|
44
|
+
</NavigationMenuPrimitive.Root>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function NavigationMenuList({
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
|
52
|
+
return (
|
|
53
|
+
<NavigationMenuPrimitive.List
|
|
54
|
+
className={cn(
|
|
55
|
+
"group flex flex-1 list-none items-center justify-center gap-1",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
data-slot="navigation-menu-list"
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function NavigationMenuItem({
|
|
65
|
+
className,
|
|
66
|
+
...props
|
|
67
|
+
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
|
68
|
+
return (
|
|
69
|
+
<NavigationMenuPrimitive.Item
|
|
70
|
+
className={cn("relative", className)}
|
|
71
|
+
data-slot="navigation-menu-item"
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const navigationMenuTriggerStyle = cva(
|
|
78
|
+
"group inline-flex h-9 w-max cursor-pointer select-none items-center justify-center gap-1 rounded-lg bg-transparent px-3 py-2 font-medium text-base outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-popup-open:bg-accent/50 data-popup-open:text-accent-foreground disabled:pointer-events-none disabled:opacity-64 sm:h-8 sm:text-sm",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
function NavigationMenuTrigger({
|
|
82
|
+
className,
|
|
83
|
+
children,
|
|
84
|
+
...props
|
|
85
|
+
}: NavigationMenuPrimitive.Trigger.Props) {
|
|
86
|
+
return (
|
|
87
|
+
<NavigationMenuPrimitive.Trigger
|
|
88
|
+
className={cn(navigationMenuTriggerStyle(), className)}
|
|
89
|
+
data-slot="navigation-menu-trigger"
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
<NavigationMenuPrimitive.Icon
|
|
94
|
+
className="relative top-px transition-transform duration-300 ease-in-out group-data-popup-open:rotate-180"
|
|
95
|
+
data-slot="navigation-menu-icon"
|
|
96
|
+
>
|
|
97
|
+
<ChevronDownIcon className="size-3.5 opacity-80" />
|
|
98
|
+
</NavigationMenuPrimitive.Icon>
|
|
99
|
+
</NavigationMenuPrimitive.Trigger>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function NavigationMenuContent({
|
|
104
|
+
className,
|
|
105
|
+
...props
|
|
106
|
+
}: NavigationMenuPrimitive.Content.Props) {
|
|
107
|
+
return (
|
|
108
|
+
<NavigationMenuPrimitive.Content
|
|
109
|
+
className={cn(
|
|
110
|
+
"h-full w-max p-2 transition-[opacity,translate,transform] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:opacity-0 data-starting-style:opacity-0 data-[activation-direction=left]:data-ending-style:translate-x-[50%] data-[activation-direction=left]:data-starting-style:translate-x-[-50%] data-[activation-direction=right]:data-ending-style:translate-x-[-50%] data-[activation-direction=right]:data-starting-style:translate-x-[50%]",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
data-slot="navigation-menu-content"
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function NavigationMenuLink({
|
|
120
|
+
className,
|
|
121
|
+
...props
|
|
122
|
+
}: NavigationMenuPrimitive.Link.Props) {
|
|
123
|
+
return (
|
|
124
|
+
<NavigationMenuPrimitive.Link
|
|
125
|
+
className={cn(
|
|
126
|
+
"flex cursor-pointer flex-col gap-1 rounded-md p-2 text-base no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background sm:text-sm [&_svg:not([class*='size-'])]:size-4",
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
data-slot="navigation-menu-link"
|
|
130
|
+
{...props}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
NavigationMenu,
|
|
137
|
+
NavigationMenuList,
|
|
138
|
+
NavigationMenuItem,
|
|
139
|
+
NavigationMenuTrigger,
|
|
140
|
+
NavigationMenuContent,
|
|
141
|
+
NavigationMenuLink,
|
|
142
|
+
navigationMenuTriggerStyle,
|
|
143
|
+
};
|