@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.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/index.cjs +6003 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2310 -0
- package/dist/index.d.ts +2310 -0
- package/dist/index.js +5828 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +96 -0
- package/dist/tailwind-preset.cjs +106 -0
- package/dist/tailwind-preset.cjs.map +1 -0
- package/dist/tailwind-preset.d.cts +23 -0
- package/dist/tailwind-preset.d.ts +23 -0
- package/dist/tailwind-preset.js +101 -0
- package/dist/tailwind-preset.js.map +1 -0
- package/package.json +154 -0
- package/src/components/accordion.tsx +187 -0
- package/src/components/alert-dialog.tsx +143 -0
- package/src/components/autocomplete.tsx +271 -0
- package/src/components/badge.tsx +62 -0
- package/src/components/button.tsx +85 -0
- package/src/components/calendar.tsx +235 -0
- package/src/components/card.tsx +94 -0
- package/src/components/checkbox.tsx +77 -0
- package/src/components/chip.tsx +77 -0
- package/src/components/confirmation-modal.tsx +195 -0
- package/src/components/context-menu.tsx +406 -0
- package/src/components/copy-button.tsx +84 -0
- package/src/components/data-grid/DataGrid.tsx +1027 -0
- package/src/components/data-grid/components/CellEditor.tsx +346 -0
- package/src/components/data-grid/components/FilterPopover.tsx +459 -0
- package/src/components/data-grid/components/HeaderCell.tsx +207 -0
- package/src/components/data-grid/components/index.ts +14 -0
- package/src/components/data-grid/hooks/index.ts +28 -0
- package/src/components/data-grid/hooks/useColumnResize.ts +378 -0
- package/src/components/data-grid/hooks/useDataGridState.ts +346 -0
- package/src/components/data-grid/hooks/useKeyboardNavigation.ts +361 -0
- package/src/components/data-grid/index.ts +71 -0
- package/src/components/data-grid/types.ts +478 -0
- package/src/components/data-grid/utils/dataProcessing.ts +277 -0
- package/src/components/data-grid/utils/index.ts +12 -0
- package/src/components/date-picker.tsx +366 -0
- package/src/components/dropdown-menu.tsx +230 -0
- package/src/components/icon-button.tsx +157 -0
- package/src/components/input.tsx +40 -0
- package/src/components/label.tsx +37 -0
- package/src/components/loading-spinner.tsx +113 -0
- package/src/components/modal.tsx +207 -0
- package/src/components/popover.tsx +62 -0
- package/src/components/progress.tsx +41 -0
- package/src/components/resizable-panel.tsx +434 -0
- package/src/components/resize-handle.tsx +187 -0
- package/src/components/select.tsx +160 -0
- package/src/components/separator.tsx +50 -0
- package/src/components/skeleton.tsx +37 -0
- package/src/components/switch.tsx +59 -0
- package/src/components/table.tsx +136 -0
- package/src/components/tabs.tsx +102 -0
- package/src/components/textarea.tsx +36 -0
- package/src/components/theme-picker.tsx +245 -0
- package/src/components/toaster.tsx +84 -0
- package/src/components/tooltip.tsx +199 -0
- package/src/index.ts +318 -0
- package/src/styles.css +96 -0
- package/src/tailwind-preset.ts +129 -0
- package/src/theme/index.ts +41 -0
- package/src/theme/presets.ts +502 -0
- package/src/theme/types.ts +164 -0
- package/src/theme/utils.ts +309 -0
- 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 };
|