@evolution-soft/ui 1.0.0 → 1.0.3

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 (78) hide show
  1. package/cli/index.js +386 -0
  2. package/components/button-icon-lottie/index.tsx +46 -0
  3. package/components/fullscreen-mode/index.tsx +82 -0
  4. package/components/header/components/buttons.tsx +102 -0
  5. package/components/header/index.tsx +146 -0
  6. package/components/loading-default/index.tsx +90 -0
  7. package/components/lottie-icon/index.tsx +78 -0
  8. package/components/not-found-default/index.tsx +68 -0
  9. package/components/settings-modal/index.tsx +225 -0
  10. package/components/sidebar/index.tsx +645 -0
  11. package/components/subtitle/index.tsx +60 -0
  12. package/components/theme-transition/index.tsx +142 -0
  13. package/components/title/index.tsx +66 -0
  14. package/components/tooltip-indicator/index.tsx +30 -0
  15. package/components/ui/accordion.tsx +66 -0
  16. package/components/ui/alert-dialog.tsx +157 -0
  17. package/components/ui/alert.tsx +66 -0
  18. package/components/ui/aspect-ratio.tsx +11 -0
  19. package/components/ui/avatar.tsx +53 -0
  20. package/components/ui/badge.tsx +46 -0
  21. package/components/ui/breadcrumb.tsx +109 -0
  22. package/components/ui/button.tsx +58 -0
  23. package/components/ui/calendar.tsx +78 -0
  24. package/components/ui/card.tsx +92 -0
  25. package/components/ui/carousel.tsx +241 -0
  26. package/components/ui/chart.tsx +360 -0
  27. package/components/ui/checkbox.tsx +32 -0
  28. package/components/ui/collapsible.tsx +33 -0
  29. package/components/ui/command.tsx +177 -0
  30. package/components/ui/context-menu.tsx +252 -0
  31. package/components/ui/dialog.tsx +135 -0
  32. package/components/ui/divisor.tsx +9 -0
  33. package/components/ui/drawer.tsx +132 -0
  34. package/components/ui/dropdown-menu.tsx +257 -0
  35. package/components/ui/emoji-picker.tsx +76 -0
  36. package/components/ui/form.tsx +168 -0
  37. package/components/ui/hover-card.tsx +44 -0
  38. package/components/ui/input-mask.tsx +46 -0
  39. package/components/ui/input-otp.tsx +77 -0
  40. package/components/ui/input.tsx +61 -0
  41. package/components/ui/label.tsx +24 -0
  42. package/components/ui/menubar.tsx +276 -0
  43. package/components/ui/multiselect.tsx +105 -0
  44. package/components/ui/navigation-menu.tsx +168 -0
  45. package/components/ui/pagination.tsx +127 -0
  46. package/components/ui/popover.tsx +48 -0
  47. package/components/ui/progress.tsx +31 -0
  48. package/components/ui/radio-group.tsx +45 -0
  49. package/components/ui/resizable.tsx +65 -0
  50. package/components/ui/scroll-area.tsx +58 -0
  51. package/components/ui/searchable-select.tsx +211 -0
  52. package/components/ui/select.tsx +189 -0
  53. package/components/ui/separator.tsx +28 -0
  54. package/components/ui/sheet.tsx +139 -0
  55. package/components/ui/sidebar.tsx +727 -0
  56. package/components/ui/skeleton.tsx +144 -0
  57. package/components/ui/slider.tsx +63 -0
  58. package/components/ui/sonner.tsx +26 -0
  59. package/components/ui/switch.tsx +31 -0
  60. package/components/ui/table.tsx +116 -0
  61. package/components/ui/tabs.tsx +76 -0
  62. package/components/ui/textarea.tsx +18 -0
  63. package/components/ui/theme-toggle.tsx +89 -0
  64. package/components/ui/toggle-group.tsx +73 -0
  65. package/components/ui/toggle.tsx +47 -0
  66. package/components/ui/tooltip.tsx +61 -0
  67. package/components/ui/use-mobile.ts +21 -0
  68. package/components/ui/utils.ts +6 -0
  69. package/contexts/AnimationSettingsContext.tsx +85 -0
  70. package/contexts/AuthContext.tsx +80 -0
  71. package/contexts/ThemeContext.tsx +70 -0
  72. package/hooks/useAnimationSettings.ts +2 -0
  73. package/hooks/usePermissions.ts +4 -0
  74. package/lib/persistentFilters.ts +120 -0
  75. package/lib/utils.ts +2 -0
  76. package/package.json +11 -2
  77. package/stores/theme.ts +30 -0
  78. 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;