@optilogic/core 1.0.0-beta.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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/index.cjs +6003 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +2310 -0
  6. package/dist/index.d.ts +2310 -0
  7. package/dist/index.js +5828 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/styles.css +96 -0
  10. package/dist/tailwind-preset.cjs +106 -0
  11. package/dist/tailwind-preset.cjs.map +1 -0
  12. package/dist/tailwind-preset.d.cts +23 -0
  13. package/dist/tailwind-preset.d.ts +23 -0
  14. package/dist/tailwind-preset.js +101 -0
  15. package/dist/tailwind-preset.js.map +1 -0
  16. package/package.json +154 -0
  17. package/src/components/accordion.tsx +187 -0
  18. package/src/components/alert-dialog.tsx +143 -0
  19. package/src/components/autocomplete.tsx +271 -0
  20. package/src/components/badge.tsx +62 -0
  21. package/src/components/button.tsx +85 -0
  22. package/src/components/calendar.tsx +235 -0
  23. package/src/components/card.tsx +94 -0
  24. package/src/components/checkbox.tsx +77 -0
  25. package/src/components/chip.tsx +77 -0
  26. package/src/components/confirmation-modal.tsx +195 -0
  27. package/src/components/context-menu.tsx +406 -0
  28. package/src/components/copy-button.tsx +84 -0
  29. package/src/components/data-grid/DataGrid.tsx +1027 -0
  30. package/src/components/data-grid/components/CellEditor.tsx +346 -0
  31. package/src/components/data-grid/components/FilterPopover.tsx +459 -0
  32. package/src/components/data-grid/components/HeaderCell.tsx +207 -0
  33. package/src/components/data-grid/components/index.ts +14 -0
  34. package/src/components/data-grid/hooks/index.ts +28 -0
  35. package/src/components/data-grid/hooks/useColumnResize.ts +378 -0
  36. package/src/components/data-grid/hooks/useDataGridState.ts +346 -0
  37. package/src/components/data-grid/hooks/useKeyboardNavigation.ts +361 -0
  38. package/src/components/data-grid/index.ts +71 -0
  39. package/src/components/data-grid/types.ts +478 -0
  40. package/src/components/data-grid/utils/dataProcessing.ts +277 -0
  41. package/src/components/data-grid/utils/index.ts +12 -0
  42. package/src/components/date-picker.tsx +366 -0
  43. package/src/components/dropdown-menu.tsx +230 -0
  44. package/src/components/icon-button.tsx +157 -0
  45. package/src/components/input.tsx +40 -0
  46. package/src/components/label.tsx +37 -0
  47. package/src/components/loading-spinner.tsx +113 -0
  48. package/src/components/modal.tsx +207 -0
  49. package/src/components/popover.tsx +62 -0
  50. package/src/components/progress.tsx +41 -0
  51. package/src/components/resizable-panel.tsx +434 -0
  52. package/src/components/resize-handle.tsx +187 -0
  53. package/src/components/select.tsx +160 -0
  54. package/src/components/separator.tsx +50 -0
  55. package/src/components/skeleton.tsx +37 -0
  56. package/src/components/switch.tsx +59 -0
  57. package/src/components/table.tsx +136 -0
  58. package/src/components/tabs.tsx +102 -0
  59. package/src/components/textarea.tsx +36 -0
  60. package/src/components/theme-picker.tsx +245 -0
  61. package/src/components/toaster.tsx +84 -0
  62. package/src/components/tooltip.tsx +199 -0
  63. package/src/index.ts +318 -0
  64. package/src/styles.css +96 -0
  65. package/src/tailwind-preset.ts +129 -0
  66. package/src/theme/index.ts +41 -0
  67. package/src/theme/presets.ts +502 -0
  68. package/src/theme/types.ts +164 -0
  69. package/src/theme/utils.ts +309 -0
  70. package/src/utils/cn.ts +14 -0
@@ -0,0 +1,187 @@
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { ChevronDown } from "lucide-react";
5
+
6
+ import { cn } from "../utils/cn";
7
+
8
+ /**
9
+ * Accordion variant styles using class-variance-authority.
10
+ * Provides consistent accordion styling across the application.
11
+ */
12
+ const accordionItemVariants = cva("", {
13
+ variants: {
14
+ variant: {
15
+ default: "border-b border-border",
16
+ bordered:
17
+ "border border-border rounded-lg mb-2 last:mb-0 overflow-hidden",
18
+ card: "bg-card border border-border rounded-lg mb-3 last:mb-0 shadow-sm overflow-hidden",
19
+ filled: "bg-muted/50 rounded-lg mb-2 last:mb-0 overflow-hidden",
20
+ ghost: "mb-1 last:mb-0",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "default",
25
+ },
26
+ });
27
+
28
+ const accordionTriggerVariants = cva(
29
+ // Base styles - no text decoration
30
+ [
31
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium",
32
+ "transition-all",
33
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
34
+ "[&[data-state=open]>svg]:rotate-180",
35
+ ],
36
+ {
37
+ variants: {
38
+ variant: {
39
+ default: "hover:text-foreground/80",
40
+ bordered: "px-4 hover:bg-muted/50",
41
+ card: "px-4 hover:bg-muted/30",
42
+ filled: "px-4 hover:bg-muted",
43
+ ghost: "px-2 rounded-md hover:bg-muted/50",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ variant: "default",
48
+ },
49
+ }
50
+ );
51
+
52
+ const accordionContentVariants = cva(
53
+ ["overflow-hidden text-sm", "data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"],
54
+ {
55
+ variants: {
56
+ variant: {
57
+ default: "",
58
+ bordered: "",
59
+ card: "",
60
+ filled: "",
61
+ ghost: "",
62
+ },
63
+ },
64
+ defaultVariants: {
65
+ variant: "default",
66
+ },
67
+ }
68
+ );
69
+
70
+ const accordionContentInnerVariants = cva("pb-4 pt-0", {
71
+ variants: {
72
+ variant: {
73
+ default: "",
74
+ bordered: "px-4",
75
+ card: "px-4",
76
+ filled: "px-4",
77
+ ghost: "px-2",
78
+ },
79
+ },
80
+ defaultVariants: {
81
+ variant: "default",
82
+ },
83
+ });
84
+
85
+ /**
86
+ * Accordion Root
87
+ *
88
+ * Container for accordion items. Can be single or multiple mode.
89
+ *
90
+ * @example
91
+ * <Accordion type="single" collapsible>
92
+ * <AccordionItem value="item-1">
93
+ * <AccordionTrigger>Section 1</AccordionTrigger>
94
+ * <AccordionContent>Content 1</AccordionContent>
95
+ * </AccordionItem>
96
+ * </Accordion>
97
+ */
98
+ const Accordion = AccordionPrimitive.Root;
99
+
100
+ export type AccordionVariant = "default" | "bordered" | "card" | "filled" | "ghost";
101
+
102
+ export interface AccordionItemProps
103
+ extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>,
104
+ VariantProps<typeof accordionItemVariants> {}
105
+
106
+ /**
107
+ * AccordionItem
108
+ *
109
+ * Individual accordion section containing a trigger and content.
110
+ * Supports variants: default, bordered, card, filled, ghost
111
+ */
112
+ const AccordionItem = React.forwardRef<
113
+ React.ElementRef<typeof AccordionPrimitive.Item>,
114
+ AccordionItemProps
115
+ >(({ className, variant, ...props }, ref) => (
116
+ <AccordionPrimitive.Item
117
+ ref={ref}
118
+ className={cn(accordionItemVariants({ variant }), className)}
119
+ data-variant={variant || "default"}
120
+ {...props}
121
+ />
122
+ ));
123
+ AccordionItem.displayName = "AccordionItem";
124
+
125
+ export interface AccordionTriggerProps
126
+ extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>,
127
+ VariantProps<typeof accordionTriggerVariants> {}
128
+
129
+ /**
130
+ * AccordionTrigger
131
+ *
132
+ * Button that toggles the accordion item open/closed.
133
+ * Includes animated chevron indicator.
134
+ * Supports variants: default, bordered, card, filled, ghost
135
+ */
136
+ const AccordionTrigger = React.forwardRef<
137
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
138
+ AccordionTriggerProps
139
+ >(({ className, children, variant, ...props }, ref) => (
140
+ <AccordionPrimitive.Header className="flex">
141
+ <AccordionPrimitive.Trigger
142
+ ref={ref}
143
+ className={cn(accordionTriggerVariants({ variant }), className)}
144
+ {...props}
145
+ >
146
+ {children}
147
+ <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
148
+ </AccordionPrimitive.Trigger>
149
+ </AccordionPrimitive.Header>
150
+ ));
151
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
152
+
153
+ export interface AccordionContentProps
154
+ extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>,
155
+ VariantProps<typeof accordionContentVariants> {}
156
+
157
+ /**
158
+ * AccordionContent
159
+ *
160
+ * Animated content area that expands/collapses.
161
+ * Supports variants: default, bordered, card, filled, ghost
162
+ */
163
+ const AccordionContent = React.forwardRef<
164
+ React.ElementRef<typeof AccordionPrimitive.Content>,
165
+ AccordionContentProps
166
+ >(({ className, children, variant, ...props }, ref) => (
167
+ <AccordionPrimitive.Content
168
+ ref={ref}
169
+ className={cn(accordionContentVariants({ variant }))}
170
+ {...props}
171
+ >
172
+ <div className={cn(accordionContentInnerVariants({ variant }), className)}>
173
+ {children}
174
+ </div>
175
+ </AccordionPrimitive.Content>
176
+ ));
177
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
178
+
179
+ export {
180
+ Accordion,
181
+ AccordionItem,
182
+ AccordionTrigger,
183
+ AccordionContent,
184
+ accordionItemVariants,
185
+ accordionTriggerVariants,
186
+ accordionContentVariants,
187
+ };
@@ -0,0 +1,143 @@
1
+ import * as React from "react";
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
+
4
+ import { cn } from "../utils/cn";
5
+ import { buttonVariants } from "./button";
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root;
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80",
20
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
21
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
+ className
23
+ )}
24
+ {...props}
25
+ ref={ref}
26
+ />
27
+ ));
28
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
29
+
30
+ const AlertDialogContent = React.forwardRef<
31
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
33
+ >(({ className, ...props }, ref) => (
34
+ <AlertDialogPortal>
35
+ <AlertDialogOverlay />
36
+ <AlertDialogPrimitive.Content
37
+ ref={ref}
38
+ className={cn(
39
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200",
40
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
41
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
42
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
43
+ "data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
44
+ "data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
45
+ "sm:rounded-lg",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </AlertDialogPortal>
51
+ ));
52
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
53
+
54
+ export interface AlertDialogHeaderProps
55
+ extends React.HTMLAttributes<HTMLDivElement> {}
56
+
57
+ const AlertDialogHeader = ({ className, ...props }: AlertDialogHeaderProps) => (
58
+ <div
59
+ className={cn(
60
+ "flex flex-col space-y-2 text-center sm:text-left",
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ );
66
+ AlertDialogHeader.displayName = "AlertDialogHeader";
67
+
68
+ export interface AlertDialogFooterProps
69
+ extends React.HTMLAttributes<HTMLDivElement> {}
70
+
71
+ const AlertDialogFooter = ({ className, ...props }: AlertDialogFooterProps) => (
72
+ <div
73
+ className={cn(
74
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ );
80
+ AlertDialogFooter.displayName = "AlertDialogFooter";
81
+
82
+ const AlertDialogTitle = React.forwardRef<
83
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
84
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
85
+ >(({ className, ...props }, ref) => (
86
+ <AlertDialogPrimitive.Title
87
+ ref={ref}
88
+ className={cn("text-lg font-semibold", className)}
89
+ {...props}
90
+ />
91
+ ));
92
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
93
+
94
+ const AlertDialogDescription = React.forwardRef<
95
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
96
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
97
+ >(({ className, ...props }, ref) => (
98
+ <AlertDialogPrimitive.Description
99
+ ref={ref}
100
+ className={cn("text-sm text-muted-foreground", className)}
101
+ {...props}
102
+ />
103
+ ));
104
+ AlertDialogDescription.displayName =
105
+ AlertDialogPrimitive.Description.displayName;
106
+
107
+ const AlertDialogAction = React.forwardRef<
108
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
109
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
110
+ >(({ className, ...props }, ref) => (
111
+ <AlertDialogPrimitive.Action
112
+ ref={ref}
113
+ className={cn(buttonVariants(), className)}
114
+ {...props}
115
+ />
116
+ ));
117
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
118
+
119
+ const AlertDialogCancel = React.forwardRef<
120
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
121
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
122
+ >(({ className, ...props }, ref) => (
123
+ <AlertDialogPrimitive.Cancel
124
+ ref={ref}
125
+ className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
126
+ {...props}
127
+ />
128
+ ));
129
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
130
+
131
+ export {
132
+ AlertDialog,
133
+ AlertDialogPortal,
134
+ AlertDialogOverlay,
135
+ AlertDialogTrigger,
136
+ AlertDialogContent,
137
+ AlertDialogHeader,
138
+ AlertDialogFooter,
139
+ AlertDialogTitle,
140
+ AlertDialogDescription,
141
+ AlertDialogAction,
142
+ AlertDialogCancel,
143
+ };
@@ -0,0 +1,271 @@
1
+ import * as React from "react";
2
+ import { Check, ChevronDown, X } from "lucide-react";
3
+
4
+ import { cn } from "../utils/cn";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
6
+
7
+ export interface AutocompleteOption {
8
+ /** Unique value for the option */
9
+ value: string;
10
+ /** Display label for the option */
11
+ label: string;
12
+ /** Optional description shown below the label */
13
+ description?: string;
14
+ /** Whether this option is disabled */
15
+ disabled?: boolean;
16
+ /** Optional group/category for the option */
17
+ group?: string;
18
+ }
19
+
20
+ export interface AutocompleteProps {
21
+ /** Array of options to display */
22
+ options: AutocompleteOption[];
23
+ /** Currently selected value */
24
+ value?: string;
25
+ /** Callback when selection changes */
26
+ onChange?: (value: string | undefined) => void;
27
+ /** Placeholder text when no selection */
28
+ placeholder?: string;
29
+ /** Placeholder for the search input */
30
+ searchPlaceholder?: string;
31
+ /** Text to show when no options match the search */
32
+ emptyText?: string;
33
+ /** Whether the autocomplete is disabled */
34
+ disabled?: boolean;
35
+ /** Additional class name for the trigger */
36
+ className?: string;
37
+ /** Whether to allow clearing the selection */
38
+ clearable?: boolean;
39
+ }
40
+
41
+ /**
42
+ * Autocomplete component - a searchable dropdown/select
43
+ *
44
+ * Features:
45
+ * - Search filtering
46
+ * - Grouped options support
47
+ * - Clearable selection
48
+ * - Keyboard navigation
49
+ *
50
+ * @example
51
+ * <Autocomplete
52
+ * options={[
53
+ * { value: 'react', label: 'React' },
54
+ * { value: 'vue', label: 'Vue' },
55
+ * ]}
56
+ * value={selected}
57
+ * onChange={setSelected}
58
+ * placeholder="Select a framework..."
59
+ * />
60
+ */
61
+ export function Autocomplete({
62
+ options,
63
+ value,
64
+ onChange,
65
+ placeholder = "Select an option...",
66
+ searchPlaceholder = "Search...",
67
+ emptyText = "No options found.",
68
+ disabled = false,
69
+ className,
70
+ clearable = false,
71
+ }: AutocompleteProps) {
72
+ const [open, setOpen] = React.useState(false);
73
+ const [search, setSearch] = React.useState("");
74
+ const inputRef = React.useRef<HTMLInputElement>(null);
75
+
76
+ // Find the selected option
77
+ const selectedOption = React.useMemo(
78
+ () => options.find((opt) => opt.value === value),
79
+ [options, value]
80
+ );
81
+
82
+ // Filter options based on search
83
+ const filteredOptions = React.useMemo(() => {
84
+ if (!search.trim()) return options;
85
+ const searchLower = search.toLowerCase();
86
+ return options.filter(
87
+ (opt) =>
88
+ opt.label.toLowerCase().includes(searchLower) ||
89
+ opt.description?.toLowerCase().includes(searchLower)
90
+ );
91
+ }, [options, search]);
92
+
93
+ // Group options if they have groups
94
+ const groupedOptions = React.useMemo(() => {
95
+ const groups: Record<string, AutocompleteOption[]> = {};
96
+ const ungrouped: AutocompleteOption[] = [];
97
+
98
+ filteredOptions.forEach((opt) => {
99
+ if (opt.group) {
100
+ if (!groups[opt.group]) groups[opt.group] = [];
101
+ groups[opt.group].push(opt);
102
+ } else {
103
+ ungrouped.push(opt);
104
+ }
105
+ });
106
+
107
+ return { groups, ungrouped };
108
+ }, [filteredOptions]);
109
+
110
+ const hasGroups = Object.keys(groupedOptions.groups).length > 0;
111
+
112
+ // Handle selection
113
+ const handleSelect = React.useCallback(
114
+ (optionValue: string) => {
115
+ onChange?.(optionValue);
116
+ setOpen(false);
117
+ setSearch("");
118
+ },
119
+ [onChange]
120
+ );
121
+
122
+ // Handle clear
123
+ const handleClear = React.useCallback(
124
+ (e: React.MouseEvent) => {
125
+ e.stopPropagation();
126
+ onChange?.(undefined);
127
+ setSearch("");
128
+ },
129
+ [onChange]
130
+ );
131
+
132
+ // Focus input when popover opens
133
+ React.useEffect(() => {
134
+ if (open) {
135
+ const timeout = setTimeout(() => {
136
+ inputRef.current?.focus();
137
+ }, 0);
138
+ return () => clearTimeout(timeout);
139
+ } else {
140
+ setSearch("");
141
+ }
142
+ }, [open]);
143
+
144
+ // Keyboard navigation
145
+ const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => {
146
+ if (e.key === "Escape") {
147
+ setOpen(false);
148
+ }
149
+ }, []);
150
+
151
+ const renderOption = (option: AutocompleteOption) => (
152
+ <button
153
+ key={option.value}
154
+ type="button"
155
+ disabled={option.disabled}
156
+ onClick={() => handleSelect(option.value)}
157
+ className={cn(
158
+ "relative flex w-full cursor-pointer select-none items-start gap-2 rounded-sm px-2 py-1.5 text-sm outline-none",
159
+ "hover:bg-accent hover:text-accent-foreground",
160
+ "focus:bg-accent focus:text-accent-foreground",
161
+ option.disabled && "pointer-events-none opacity-50",
162
+ value === option.value && "bg-accent/50"
163
+ )}
164
+ >
165
+ <span className="flex h-4 w-4 items-center justify-center flex-shrink-0 mt-0.5">
166
+ {value === option.value && <Check className="h-4 w-4" />}
167
+ </span>
168
+ <div className="flex-1 min-w-0">
169
+ <div className="truncate">{option.label}</div>
170
+ {option.description && (
171
+ <div className="text-xs text-muted-foreground truncate">
172
+ {option.description}
173
+ </div>
174
+ )}
175
+ </div>
176
+ </button>
177
+ );
178
+
179
+ return (
180
+ <Popover open={open} onOpenChange={setOpen}>
181
+ <PopoverTrigger asChild disabled={disabled}>
182
+ <button
183
+ type="button"
184
+ role="combobox"
185
+ aria-expanded={open}
186
+ aria-haspopup="listbox"
187
+ disabled={disabled}
188
+ className={cn(
189
+ "flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background",
190
+ "focus:outline-none focus:ring-1 focus:ring-ring",
191
+ "disabled:cursor-not-allowed disabled:opacity-50",
192
+ !selectedOption && "text-muted-foreground",
193
+ className
194
+ )}
195
+ >
196
+ <span className="truncate flex-1 text-left">
197
+ {selectedOption?.label || placeholder}
198
+ </span>
199
+ <div className="flex items-center gap-1 flex-shrink-0">
200
+ {clearable && selectedOption && (
201
+ <span
202
+ role="button"
203
+ tabIndex={-1}
204
+ onClick={handleClear}
205
+ className="rounded-sm hover:bg-muted p-0.5"
206
+ >
207
+ <X className="h-3.5 w-3.5 text-muted-foreground" />
208
+ </span>
209
+ )}
210
+ <ChevronDown className="h-4 w-4 opacity-50" />
211
+ </div>
212
+ </button>
213
+ </PopoverTrigger>
214
+ <PopoverContent
215
+ className="w-[--radix-popover-trigger-width] p-0"
216
+ align="start"
217
+ sideOffset={4}
218
+ onKeyDown={handleKeyDown}
219
+ >
220
+ {/* Search input */}
221
+ <div className="flex items-center border-b border-border px-3">
222
+ <input
223
+ ref={inputRef}
224
+ type="text"
225
+ value={search}
226
+ onChange={(e) => setSearch(e.target.value)}
227
+ placeholder={searchPlaceholder}
228
+ className="flex h-9 w-full bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground"
229
+ />
230
+ {search && (
231
+ <button
232
+ type="button"
233
+ onClick={() => setSearch("")}
234
+ className="p-1 hover:bg-muted rounded-sm"
235
+ >
236
+ <X className="h-3.5 w-3.5 text-muted-foreground" />
237
+ </button>
238
+ )}
239
+ </div>
240
+
241
+ {/* Options list */}
242
+ <div className="max-h-[300px] overflow-y-auto p-1">
243
+ {filteredOptions.length === 0 ? (
244
+ <div className="py-6 text-center text-sm text-muted-foreground">
245
+ {emptyText}
246
+ </div>
247
+ ) : hasGroups ? (
248
+ <>
249
+ {/* Ungrouped options first */}
250
+ {groupedOptions.ungrouped.map(renderOption)}
251
+
252
+ {/* Grouped options */}
253
+ {Object.entries(groupedOptions.groups).map(([group, opts]) => (
254
+ <div key={group}>
255
+ <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
256
+ {group}
257
+ </div>
258
+ {opts.map(renderOption)}
259
+ </div>
260
+ ))}
261
+ </>
262
+ ) : (
263
+ filteredOptions.map(renderOption)
264
+ )}
265
+ </div>
266
+ </PopoverContent>
267
+ </Popover>
268
+ );
269
+ }
270
+
271
+ export default Autocomplete;
@@ -0,0 +1,62 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "../utils/cn";
5
+
6
+ /**
7
+ * Badge variant styles using class-variance-authority.
8
+ */
9
+ const badgeVariants = cva(
10
+ "inline-flex items-center rounded border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default:
15
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
16
+ secondary:
17
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
+ destructive:
19
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
20
+ outline: "text-foreground",
21
+ success:
22
+ "border-transparent bg-success text-success-foreground shadow hover:bg-success/80",
23
+ warning:
24
+ "border-transparent bg-warning text-warning-foreground shadow hover:bg-warning/80",
25
+ muted:
26
+ "border-transparent bg-muted text-muted-foreground hover:bg-muted/80",
27
+ accent:
28
+ "border-transparent bg-accent text-accent-foreground hover:bg-accent/80",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ },
34
+ }
35
+ );
36
+
37
+ export interface BadgeProps
38
+ extends React.HTMLAttributes<HTMLDivElement>,
39
+ VariantProps<typeof badgeVariants> {}
40
+
41
+ /**
42
+ * Badge Component
43
+ *
44
+ * A small status indicator with various semantic variants.
45
+ * Use for tags, labels, counts, or status indicators.
46
+ *
47
+ * @example
48
+ * <Badge>Default</Badge>
49
+ *
50
+ * @example
51
+ * <Badge variant="success">Active</Badge>
52
+ *
53
+ * @example
54
+ * <Badge variant="destructive">Error</Badge>
55
+ */
56
+ function Badge({ className, variant, ...props }: BadgeProps) {
57
+ return (
58
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
59
+ );
60
+ }
61
+
62
+ export { Badge, badgeVariants };