@evolution-soft/ui 1.0.0 → 1.0.1
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/cli/index.mjs +386 -0
- package/components/button-icon-lottie/index.tsx +46 -0
- package/components/fullscreen-mode/index.tsx +82 -0
- package/components/header/components/buttons.tsx +102 -0
- package/components/header/index.tsx +146 -0
- package/components/loading-default/index.tsx +90 -0
- package/components/lottie-icon/index.tsx +78 -0
- package/components/not-found-default/index.tsx +68 -0
- package/components/settings-modal/index.tsx +225 -0
- package/components/sidebar/index.tsx +645 -0
- package/components/subtitle/index.tsx +60 -0
- package/components/theme-transition/index.tsx +142 -0
- package/components/title/index.tsx +66 -0
- package/components/tooltip-indicator/index.tsx +30 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/calendar.tsx +78 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +360 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +135 -0
- package/components/ui/divisor.tsx +9 -0
- package/components/ui/drawer.tsx +132 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/emoji-picker.tsx +76 -0
- package/components/ui/form.tsx +168 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-mask.tsx +46 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +61 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/multiselect.tsx +105 -0
- package/components/ui/navigation-menu.tsx +168 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +45 -0
- package/components/ui/resizable.tsx +65 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/searchable-select.tsx +211 -0
- package/components/ui/select.tsx +189 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +139 -0
- package/components/ui/sidebar.tsx +727 -0
- package/components/ui/skeleton.tsx +144 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/sonner.tsx +26 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +76 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/theme-toggle.tsx +89 -0
- package/components/ui/toggle-group.tsx +73 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/use-mobile.ts +21 -0
- package/components/ui/utils.ts +6 -0
- package/contexts/AnimationSettingsContext.tsx +85 -0
- package/contexts/AuthContext.tsx +80 -0
- package/contexts/ThemeContext.tsx +70 -0
- package/hooks/useAnimationSettings.ts +2 -0
- package/hooks/usePermissions.ts +4 -0
- package/lib/persistentFilters.ts +120 -0
- package/lib/utils.ts +2 -0
- package/package.json +11 -2
- package/stores/theme.ts +30 -0
- package/stores/useThemeStore.ts +32 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
ChevronLeftIcon,
|
|
4
|
+
ChevronRightIcon,
|
|
5
|
+
MoreHorizontalIcon,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "./utils";
|
|
9
|
+
import { Button, buttonVariants } from "./button";
|
|
10
|
+
|
|
11
|
+
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
|
12
|
+
return (
|
|
13
|
+
<nav
|
|
14
|
+
role="navigation"
|
|
15
|
+
aria-label="pagination"
|
|
16
|
+
data-slot="pagination"
|
|
17
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function PaginationContent({
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"ul">) {
|
|
27
|
+
return (
|
|
28
|
+
<ul
|
|
29
|
+
data-slot="pagination-content"
|
|
30
|
+
className={cn("flex flex-row items-center gap-1", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
|
37
|
+
return <li data-slot="pagination-item" {...props} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type PaginationLinkProps = {
|
|
41
|
+
isActive?: boolean;
|
|
42
|
+
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
|
43
|
+
React.ComponentProps<"a">;
|
|
44
|
+
|
|
45
|
+
function PaginationLink({
|
|
46
|
+
className,
|
|
47
|
+
isActive,
|
|
48
|
+
size = "icon",
|
|
49
|
+
...props
|
|
50
|
+
}: PaginationLinkProps) {
|
|
51
|
+
return (
|
|
52
|
+
<a
|
|
53
|
+
aria-current={isActive ? "page" : undefined}
|
|
54
|
+
data-slot="pagination-link"
|
|
55
|
+
data-active={isActive}
|
|
56
|
+
className={cn(
|
|
57
|
+
buttonVariants({
|
|
58
|
+
variant: isActive ? "outline" : "outline",
|
|
59
|
+
size,
|
|
60
|
+
}),
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function PaginationPrevious({
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: React.ComponentProps<typeof PaginationLink>) {
|
|
72
|
+
return (
|
|
73
|
+
<PaginationLink
|
|
74
|
+
aria-label="Go to previous page"
|
|
75
|
+
size="default"
|
|
76
|
+
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
<ChevronLeftIcon />
|
|
80
|
+
<span className="hidden sm:block">Previous</span>
|
|
81
|
+
</PaginationLink>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function PaginationNext({
|
|
86
|
+
className,
|
|
87
|
+
...props
|
|
88
|
+
}: React.ComponentProps<typeof PaginationLink>) {
|
|
89
|
+
return (
|
|
90
|
+
<PaginationLink
|
|
91
|
+
aria-label="Go to next page"
|
|
92
|
+
size="default"
|
|
93
|
+
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
|
94
|
+
{...props}
|
|
95
|
+
>
|
|
96
|
+
<span className="hidden sm:block">Next</span>
|
|
97
|
+
<ChevronRightIcon />
|
|
98
|
+
</PaginationLink>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function PaginationEllipsis({
|
|
103
|
+
className,
|
|
104
|
+
...props
|
|
105
|
+
}: React.ComponentProps<"span">) {
|
|
106
|
+
return (
|
|
107
|
+
<span
|
|
108
|
+
aria-hidden
|
|
109
|
+
data-slot="pagination-ellipsis"
|
|
110
|
+
className={cn("flex size-9 items-center justify-center", className)}
|
|
111
|
+
{...props}
|
|
112
|
+
>
|
|
113
|
+
<MoreHorizontalIcon className="size-4" />
|
|
114
|
+
<span className="sr-only">More pages</span>
|
|
115
|
+
</span>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
Pagination,
|
|
121
|
+
PaginationContent,
|
|
122
|
+
PaginationLink,
|
|
123
|
+
PaginationItem,
|
|
124
|
+
PaginationPrevious,
|
|
125
|
+
PaginationNext,
|
|
126
|
+
PaginationEllipsis,
|
|
127
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
function Popover({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
11
|
+
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function PopoverTrigger({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
|
17
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function PopoverContent({
|
|
21
|
+
className,
|
|
22
|
+
align = "center",
|
|
23
|
+
sideOffset = 4,
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
26
|
+
return (
|
|
27
|
+
<PopoverPrimitive.Portal>
|
|
28
|
+
<PopoverPrimitive.Content
|
|
29
|
+
data-slot="popover-content"
|
|
30
|
+
align={align}
|
|
31
|
+
sideOffset={sideOffset}
|
|
32
|
+
className={cn(
|
|
33
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
</PopoverPrimitive.Portal>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function PopoverAnchor({
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
|
45
|
+
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
function Progress({
|
|
9
|
+
className,
|
|
10
|
+
value,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<ProgressPrimitive.Root
|
|
15
|
+
data-slot="progress"
|
|
16
|
+
className={cn(
|
|
17
|
+
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<ProgressPrimitive.Indicator
|
|
23
|
+
data-slot="progress-indicator"
|
|
24
|
+
className="bg-primary h-full w-full flex-1 transition-all"
|
|
25
|
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
26
|
+
/>
|
|
27
|
+
</ProgressPrimitive.Root>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { Progress };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
5
|
+
import { CircleIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "./utils";
|
|
8
|
+
|
|
9
|
+
function RadioGroup({
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<RadioGroupPrimitive.Root
|
|
15
|
+
data-slot="radio-group"
|
|
16
|
+
className={cn("grid gap-3", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function RadioGroupItem({
|
|
23
|
+
className,
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
|
26
|
+
return (
|
|
27
|
+
<RadioGroupPrimitive.Item
|
|
28
|
+
data-slot="radio-group-item"
|
|
29
|
+
className={cn(
|
|
30
|
+
"cursor-pointer border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<RadioGroupPrimitive.Indicator
|
|
36
|
+
data-slot="radio-group-indicator"
|
|
37
|
+
className="relative flex items-center justify-center"
|
|
38
|
+
>
|
|
39
|
+
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
|
40
|
+
</RadioGroupPrimitive.Indicator>
|
|
41
|
+
</RadioGroupPrimitive.Item>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { RadioGroup, RadioGroupItem };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { GripVerticalIcon } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
Group as ResizableGroup,
|
|
5
|
+
Panel as ResizablePanelPrimitive,
|
|
6
|
+
Separator as ResizableSeparator,
|
|
7
|
+
} from "react-resizable-panels";
|
|
8
|
+
|
|
9
|
+
import { cn } from "./utils";
|
|
10
|
+
|
|
11
|
+
function ResizablePanelGroup({
|
|
12
|
+
className,
|
|
13
|
+
direction,
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof ResizableGroup> & {
|
|
16
|
+
direction?: "horizontal" | "vertical";
|
|
17
|
+
}) {
|
|
18
|
+
const orientation = direction ?? props.orientation;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<ResizableGroup
|
|
22
|
+
data-slot="resizable-panel-group"
|
|
23
|
+
data-panel-group-direction={orientation}
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
orientation={orientation}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ResizablePanel({
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof ResizablePanelPrimitive>) {
|
|
37
|
+
return <ResizablePanelPrimitive data-slot="resizable-panel" {...props} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ResizableHandle({
|
|
41
|
+
withHandle,
|
|
42
|
+
className,
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<typeof ResizableSeparator> & {
|
|
45
|
+
withHandle?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<ResizableSeparator
|
|
49
|
+
data-slot="resizable-handle"
|
|
50
|
+
className={cn(
|
|
51
|
+
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{withHandle && (
|
|
57
|
+
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
|
58
|
+
<GripVerticalIcon className="size-2.5" />
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</ResizableSeparator>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
function ScrollArea({
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<ScrollAreaPrimitive.Root
|
|
15
|
+
data-slot="scroll-area"
|
|
16
|
+
className={cn("relative", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<ScrollAreaPrimitive.Viewport
|
|
20
|
+
data-slot="scroll-area-viewport"
|
|
21
|
+
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</ScrollAreaPrimitive.Viewport>
|
|
25
|
+
<ScrollBar />
|
|
26
|
+
<ScrollAreaPrimitive.Corner />
|
|
27
|
+
</ScrollAreaPrimitive.Root>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ScrollBar({
|
|
32
|
+
className,
|
|
33
|
+
orientation = "vertical",
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
|
36
|
+
return (
|
|
37
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
38
|
+
data-slot="scroll-area-scrollbar"
|
|
39
|
+
orientation={orientation}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex touch-none p-px transition-colors select-none",
|
|
42
|
+
orientation === "vertical" &&
|
|
43
|
+
"h-full w-2.5 border-l border-l-transparent",
|
|
44
|
+
orientation === "horizontal" &&
|
|
45
|
+
"h-2.5 flex-col border-t border-t-transparent",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
<ScrollAreaPrimitive.ScrollAreaThumb
|
|
51
|
+
data-slot="scroll-area-thumb"
|
|
52
|
+
className="bg-border relative flex-1 rounded-full"
|
|
53
|
+
/>
|
|
54
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { ScrollArea, ScrollBar };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Search } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
import { Input } from "./input";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from "./select";
|
|
14
|
+
|
|
15
|
+
export interface SearchableSelectItem {
|
|
16
|
+
value: string;
|
|
17
|
+
label: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SearchableSelectProps {
|
|
23
|
+
items: SearchableSelectItem[];
|
|
24
|
+
value?: string;
|
|
25
|
+
onValueChange?: (value: string) => void;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
searchPlaceholder?: string;
|
|
28
|
+
emptyMessage?: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
searchFunction?: (item: SearchableSelectItem, searchTerm: string) => boolean;
|
|
32
|
+
onSearchChange?: (searchTerm: string) => void;
|
|
33
|
+
isLoading?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultSearchFunction = (item: SearchableSelectItem, searchTerm: string): boolean => {
|
|
37
|
+
const term = searchTerm.toLowerCase();
|
|
38
|
+
return (
|
|
39
|
+
item.label.toLowerCase().includes(term) ||
|
|
40
|
+
Boolean(item.description && item.description.toLowerCase().includes(term))
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function SearchableSelect({
|
|
45
|
+
items,
|
|
46
|
+
value,
|
|
47
|
+
onValueChange,
|
|
48
|
+
placeholder = "Selecione uma opção...",
|
|
49
|
+
searchPlaceholder = "Pesquisar...",
|
|
50
|
+
emptyMessage = "Nenhum resultado encontrado",
|
|
51
|
+
className,
|
|
52
|
+
disabled = false,
|
|
53
|
+
searchFunction = defaultSearchFunction,
|
|
54
|
+
onSearchChange,
|
|
55
|
+
isLoading = false,
|
|
56
|
+
}: SearchableSelectProps) {
|
|
57
|
+
const [searchTerm, setSearchTerm] = React.useState("");
|
|
58
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
59
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
60
|
+
const debounceTimerRef = React.useRef<NodeJS.Timeout | null>(null);
|
|
61
|
+
|
|
62
|
+
// Chama onSearchChange com debounce quando fornecido
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (onSearchChange) {
|
|
65
|
+
if (debounceTimerRef.current) {
|
|
66
|
+
clearTimeout(debounceTimerRef.current);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
70
|
+
onSearchChange(searchTerm);
|
|
71
|
+
}, 300);
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
if (debounceTimerRef.current) {
|
|
75
|
+
clearTimeout(debounceTimerRef.current);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}, [searchTerm, onSearchChange]);
|
|
80
|
+
|
|
81
|
+
const filteredItems = React.useMemo(() => {
|
|
82
|
+
// Se tem onSearchChange, não filtra localmente (a filtragem vem da API)
|
|
83
|
+
if (onSearchChange) {
|
|
84
|
+
return items;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!searchTerm.trim()) {
|
|
88
|
+
return items;
|
|
89
|
+
}
|
|
90
|
+
return items.filter((item) => searchFunction(item, searchTerm));
|
|
91
|
+
}, [items, searchTerm, searchFunction, onSearchChange]);
|
|
92
|
+
|
|
93
|
+
const selectedItem = items.find(item => item.value === value);
|
|
94
|
+
|
|
95
|
+
const handleSelect = (selectedValue: string) => {
|
|
96
|
+
onValueChange?.(selectedValue);
|
|
97
|
+
setIsOpen(false);
|
|
98
|
+
setSearchTerm("");
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleOpenChange = (open: boolean) => {
|
|
102
|
+
setIsOpen(open);
|
|
103
|
+
if (!open) {
|
|
104
|
+
setSearchTerm("");
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Select
|
|
110
|
+
value={value}
|
|
111
|
+
onValueChange={handleSelect}
|
|
112
|
+
onOpenChange={handleOpenChange}
|
|
113
|
+
open={isOpen}
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
>
|
|
116
|
+
<SelectTrigger className={cn("w-full", className)}>
|
|
117
|
+
<SelectValue placeholder={placeholder}>
|
|
118
|
+
{selectedItem && (
|
|
119
|
+
<div className="flex flex-col items-start">
|
|
120
|
+
<span className="font-medium">{selectedItem.label}</span>
|
|
121
|
+
{selectedItem.description && (
|
|
122
|
+
<span className="text-xs text-muted-foreground">
|
|
123
|
+
{selectedItem.description}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</SelectValue>
|
|
129
|
+
</SelectTrigger>
|
|
130
|
+
<SelectContent>
|
|
131
|
+
{/* Campo de pesquisa */}
|
|
132
|
+
<div
|
|
133
|
+
className="flex items-center border-b px-3 pb-2"
|
|
134
|
+
onKeyDown={(e) => {
|
|
135
|
+
// Previne a navegação por teclado do Select quando estiver digitando
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<Search className="mr-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
140
|
+
<Input
|
|
141
|
+
ref={inputRef}
|
|
142
|
+
placeholder={searchPlaceholder}
|
|
143
|
+
value={searchTerm}
|
|
144
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
145
|
+
className="h-8 border-0 p-0 text-sm focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
146
|
+
autoFocus
|
|
147
|
+
onKeyDown={(e) => {
|
|
148
|
+
// Previne que Enter feche o select enquanto digita
|
|
149
|
+
if (e.key === 'Enter' && searchTerm) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
// Se houver apenas um resultado, seleciona ele
|
|
152
|
+
if (filteredItems.length === 1) {
|
|
153
|
+
handleSelect(filteredItems[0].value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Previne que Escape limpe o input antes de fechar
|
|
157
|
+
if (e.key === 'Escape' && searchTerm) {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
setSearchTerm("");
|
|
160
|
+
}
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Lista de itens filtrados */}
|
|
166
|
+
<div className="max-h-50 overflow-auto">
|
|
167
|
+
{isLoading ? (
|
|
168
|
+
<div className="py-6 text-center">
|
|
169
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-gray-100 mx-auto"></div>
|
|
170
|
+
<p className="text-xs text-muted-foreground mt-2">Carregando...</p>
|
|
171
|
+
</div>
|
|
172
|
+
) : filteredItems.length > 0 ? (
|
|
173
|
+
filteredItems.map((item) => (
|
|
174
|
+
<SelectItem
|
|
175
|
+
key={item.value}
|
|
176
|
+
value={item.value}
|
|
177
|
+
disabled={item.disabled}
|
|
178
|
+
className={cn(
|
|
179
|
+
"flex flex-col items-start py-2",
|
|
180
|
+
item.description && "h-auto"
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
<div className="flex flex-col items-start w-full">
|
|
184
|
+
<span className="font-medium">{item.label}</span>
|
|
185
|
+
{item.description && (
|
|
186
|
+
<span className="text-xs text-muted-foreground mt-0.5">
|
|
187
|
+
{item.description}
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</SelectItem>
|
|
192
|
+
))
|
|
193
|
+
) : (
|
|
194
|
+
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
195
|
+
{emptyMessage}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Contador de resultados */}
|
|
201
|
+
{searchTerm && (
|
|
202
|
+
<div className="border-t px-3 py-2 text-xs text-muted-foreground">
|
|
203
|
+
{filteredItems.length} resultado(s) encontrado(s)
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</SelectContent>
|
|
207
|
+
</Select>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default SearchableSelect;
|