@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,230 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
3
|
+
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
|
|
7
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
8
|
+
|
|
9
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
10
|
+
|
|
11
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
12
|
+
|
|
13
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
14
|
+
|
|
15
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
16
|
+
|
|
17
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
18
|
+
|
|
19
|
+
export interface DropdownMenuSubTriggerProps
|
|
20
|
+
extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> {
|
|
21
|
+
inset?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
25
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
26
|
+
DropdownMenuSubTriggerProps
|
|
27
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
28
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
|
32
|
+
"focus:bg-accent",
|
|
33
|
+
"data-[state=open]:bg-accent",
|
|
34
|
+
inset && "pl-8",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
41
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
42
|
+
));
|
|
43
|
+
DropdownMenuSubTrigger.displayName =
|
|
44
|
+
DropdownMenuPrimitive.SubTrigger.displayName;
|
|
45
|
+
|
|
46
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
47
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
48
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
49
|
+
>(({ className, ...props }, ref) => (
|
|
50
|
+
<DropdownMenuPrimitive.SubContent
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn(
|
|
53
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg",
|
|
54
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
55
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
56
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
57
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
58
|
+
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
));
|
|
64
|
+
DropdownMenuSubContent.displayName =
|
|
65
|
+
DropdownMenuPrimitive.SubContent.displayName;
|
|
66
|
+
|
|
67
|
+
const DropdownMenuContent = React.forwardRef<
|
|
68
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
69
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
70
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
71
|
+
<DropdownMenuPrimitive.Portal>
|
|
72
|
+
<DropdownMenuPrimitive.Content
|
|
73
|
+
ref={ref}
|
|
74
|
+
sideOffset={sideOffset}
|
|
75
|
+
className={cn(
|
|
76
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md",
|
|
77
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
78
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
79
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
80
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
81
|
+
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
</DropdownMenuPrimitive.Portal>
|
|
87
|
+
));
|
|
88
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
89
|
+
|
|
90
|
+
export interface DropdownMenuItemProps
|
|
91
|
+
extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> {
|
|
92
|
+
inset?: boolean;
|
|
93
|
+
destructive?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DropdownMenuItem = React.forwardRef<
|
|
97
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
98
|
+
DropdownMenuItemProps
|
|
99
|
+
>(({ className, inset, destructive, ...props }, ref) => (
|
|
100
|
+
<DropdownMenuPrimitive.Item
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn(
|
|
103
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors",
|
|
104
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
105
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
106
|
+
inset && "pl-8",
|
|
107
|
+
destructive && "text-destructive focus:text-destructive",
|
|
108
|
+
className
|
|
109
|
+
)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
));
|
|
113
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
114
|
+
|
|
115
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
116
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
117
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
118
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
119
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
120
|
+
ref={ref}
|
|
121
|
+
className={cn(
|
|
122
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors",
|
|
123
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
124
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
125
|
+
className
|
|
126
|
+
)}
|
|
127
|
+
checked={checked}
|
|
128
|
+
{...props}
|
|
129
|
+
>
|
|
130
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
131
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
132
|
+
<Check className="h-4 w-4" />
|
|
133
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
134
|
+
</span>
|
|
135
|
+
{children}
|
|
136
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
137
|
+
));
|
|
138
|
+
DropdownMenuCheckboxItem.displayName =
|
|
139
|
+
DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
140
|
+
|
|
141
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
142
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
143
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
144
|
+
>(({ className, children, ...props }, ref) => (
|
|
145
|
+
<DropdownMenuPrimitive.RadioItem
|
|
146
|
+
ref={ref}
|
|
147
|
+
className={cn(
|
|
148
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors",
|
|
149
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
150
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
156
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
157
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
158
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
159
|
+
</span>
|
|
160
|
+
{children}
|
|
161
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
162
|
+
));
|
|
163
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
164
|
+
|
|
165
|
+
export interface DropdownMenuLabelProps
|
|
166
|
+
extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> {
|
|
167
|
+
inset?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
171
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
172
|
+
DropdownMenuLabelProps
|
|
173
|
+
>(({ className, inset, ...props }, ref) => (
|
|
174
|
+
<DropdownMenuPrimitive.Label
|
|
175
|
+
ref={ref}
|
|
176
|
+
className={cn(
|
|
177
|
+
"px-2 py-1.5 text-sm font-semibold",
|
|
178
|
+
inset && "pl-8",
|
|
179
|
+
className
|
|
180
|
+
)}
|
|
181
|
+
{...props}
|
|
182
|
+
/>
|
|
183
|
+
));
|
|
184
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
185
|
+
|
|
186
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
187
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
188
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
189
|
+
>(({ className, ...props }, ref) => (
|
|
190
|
+
<DropdownMenuPrimitive.Separator
|
|
191
|
+
ref={ref}
|
|
192
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
193
|
+
{...props}
|
|
194
|
+
/>
|
|
195
|
+
));
|
|
196
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
197
|
+
|
|
198
|
+
export interface DropdownMenuShortcutProps
|
|
199
|
+
extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
200
|
+
|
|
201
|
+
const DropdownMenuShortcut = ({
|
|
202
|
+
className,
|
|
203
|
+
...props
|
|
204
|
+
}: DropdownMenuShortcutProps) => {
|
|
205
|
+
return (
|
|
206
|
+
<span
|
|
207
|
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
208
|
+
{...props}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
|
213
|
+
|
|
214
|
+
export {
|
|
215
|
+
DropdownMenu,
|
|
216
|
+
DropdownMenuTrigger,
|
|
217
|
+
DropdownMenuContent,
|
|
218
|
+
DropdownMenuItem,
|
|
219
|
+
DropdownMenuCheckboxItem,
|
|
220
|
+
DropdownMenuRadioItem,
|
|
221
|
+
DropdownMenuLabel,
|
|
222
|
+
DropdownMenuSeparator,
|
|
223
|
+
DropdownMenuShortcut,
|
|
224
|
+
DropdownMenuGroup,
|
|
225
|
+
DropdownMenuPortal,
|
|
226
|
+
DropdownMenuSub,
|
|
227
|
+
DropdownMenuSubContent,
|
|
228
|
+
DropdownMenuSubTrigger,
|
|
229
|
+
DropdownMenuRadioGroup,
|
|
230
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconButton Component
|
|
3
|
+
*
|
|
4
|
+
* A consistent icon button primitive for toolbars and actions.
|
|
5
|
+
* Features:
|
|
6
|
+
* - Multiple size variants
|
|
7
|
+
* - Multiple style variants
|
|
8
|
+
* - Consistent hover states
|
|
9
|
+
* - Theme-aware styling
|
|
10
|
+
* - Optional active state
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
15
|
+
|
|
16
|
+
import { cn } from "../utils/cn";
|
|
17
|
+
|
|
18
|
+
const iconButtonVariants = cva(
|
|
19
|
+
// Base styles
|
|
20
|
+
[
|
|
21
|
+
"inline-flex items-center justify-center",
|
|
22
|
+
"rounded-md",
|
|
23
|
+
"transition-colors duration-150",
|
|
24
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
25
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
26
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
27
|
+
],
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
/**
|
|
32
|
+
* Default - subtle background with accent on hover
|
|
33
|
+
*/
|
|
34
|
+
default: [
|
|
35
|
+
"bg-transparent text-muted-foreground",
|
|
36
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
37
|
+
"border border-transparent",
|
|
38
|
+
],
|
|
39
|
+
/**
|
|
40
|
+
* Ghost - no background, accent on hover
|
|
41
|
+
*/
|
|
42
|
+
ghost: [
|
|
43
|
+
"bg-transparent text-muted-foreground",
|
|
44
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
45
|
+
],
|
|
46
|
+
/**
|
|
47
|
+
* Outline - border with accent on hover
|
|
48
|
+
*/
|
|
49
|
+
outline: [
|
|
50
|
+
"bg-transparent text-muted-foreground",
|
|
51
|
+
"border border-border",
|
|
52
|
+
"hover:bg-accent hover:text-accent-foreground hover:border-accent",
|
|
53
|
+
],
|
|
54
|
+
/**
|
|
55
|
+
* Filled - accent background
|
|
56
|
+
*/
|
|
57
|
+
filled: [
|
|
58
|
+
"bg-accent text-accent-foreground",
|
|
59
|
+
"hover:bg-accent/90 hover:shadow-sm",
|
|
60
|
+
],
|
|
61
|
+
/**
|
|
62
|
+
* Muted - muted background with accent on hover
|
|
63
|
+
*/
|
|
64
|
+
muted: [
|
|
65
|
+
"bg-muted text-muted-foreground",
|
|
66
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
size: {
|
|
70
|
+
/**
|
|
71
|
+
* Small - 32px (8 × 4)
|
|
72
|
+
*/
|
|
73
|
+
sm: "h-8 w-8 [&_svg]:h-4 [&_svg]:w-4",
|
|
74
|
+
/**
|
|
75
|
+
* Default - 36px (9 × 4)
|
|
76
|
+
*/
|
|
77
|
+
default: "h-9 w-9 [&_svg]:h-4.5 [&_svg]:w-4.5",
|
|
78
|
+
/**
|
|
79
|
+
* Large - 40px (10 × 4)
|
|
80
|
+
*/
|
|
81
|
+
lg: "h-10 w-10 [&_svg]:h-5 [&_svg]:w-5",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
defaultVariants: {
|
|
85
|
+
variant: "default",
|
|
86
|
+
size: "default",
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export interface IconButtonProps
|
|
92
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
93
|
+
VariantProps<typeof iconButtonVariants> {
|
|
94
|
+
/**
|
|
95
|
+
* Whether the button is in an active/selected state
|
|
96
|
+
*/
|
|
97
|
+
isActive?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Icon element to display (typically from lucide-react)
|
|
100
|
+
*/
|
|
101
|
+
icon?: React.ReactNode;
|
|
102
|
+
/**
|
|
103
|
+
* Accessibility label (required if no children)
|
|
104
|
+
*/
|
|
105
|
+
"aria-label": string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* IconButton component
|
|
110
|
+
*
|
|
111
|
+
* A versatile icon button for toolbars and action buttons.
|
|
112
|
+
* Uses consistent hover states across all themes.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* <IconButton
|
|
116
|
+
* icon={<RefreshCw className="w-4 h-4" />}
|
|
117
|
+
* aria-label="Refresh"
|
|
118
|
+
* onClick={handleRefresh}
|
|
119
|
+
* />
|
|
120
|
+
*
|
|
121
|
+
* @example With active state
|
|
122
|
+
* <IconButton
|
|
123
|
+
* icon={<Grid className="w-4 h-4" />}
|
|
124
|
+
* aria-label="Grid view"
|
|
125
|
+
* isActive={viewMode === 'grid'}
|
|
126
|
+
* onClick={() => setViewMode('grid')}
|
|
127
|
+
* />
|
|
128
|
+
*/
|
|
129
|
+
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
130
|
+
(
|
|
131
|
+
{ className, variant, size, isActive = false, icon, children, ...props },
|
|
132
|
+
ref
|
|
133
|
+
) => {
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
className={cn(
|
|
137
|
+
iconButtonVariants({ variant, size }),
|
|
138
|
+
// Active state overrides
|
|
139
|
+
isActive && [
|
|
140
|
+
"bg-accent text-accent-foreground",
|
|
141
|
+
// Add subtle shadow for visual feedback
|
|
142
|
+
variant !== "filled" && "shadow-sm",
|
|
143
|
+
],
|
|
144
|
+
className
|
|
145
|
+
)}
|
|
146
|
+
ref={ref}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
{icon || children}
|
|
150
|
+
</button>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
IconButton.displayName = "IconButton";
|
|
156
|
+
|
|
157
|
+
export { IconButton, iconButtonVariants };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../utils/cn";
|
|
4
|
+
|
|
5
|
+
export interface InputProps
|
|
6
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Input Component
|
|
10
|
+
*
|
|
11
|
+
* A styled text input with consistent theming.
|
|
12
|
+
* Supports all native input attributes.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <Input placeholder="Enter your name" />
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <Input type="email" placeholder="email@example.com" />
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <Input disabled value="Disabled input" />
|
|
22
|
+
*/
|
|
23
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
24
|
+
({ className, type, ...props }, ref) => {
|
|
25
|
+
return (
|
|
26
|
+
<input
|
|
27
|
+
type={type}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
ref={ref}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
Input.displayName = "Input";
|
|
39
|
+
|
|
40
|
+
export { Input };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
|
|
7
|
+
const labelVariants = cva(
|
|
8
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export interface LabelProps
|
|
12
|
+
extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>,
|
|
13
|
+
VariantProps<typeof labelVariants> {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Label Component
|
|
17
|
+
*
|
|
18
|
+
* A form label component with consistent styling.
|
|
19
|
+
* Automatically associates with form controls via htmlFor.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <Label htmlFor="email">Email</Label>
|
|
23
|
+
* <Input id="email" type="email" />
|
|
24
|
+
*/
|
|
25
|
+
const Label = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
27
|
+
LabelProps
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<LabelPrimitive.Root
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(labelVariants(), className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
));
|
|
35
|
+
Label.displayName = LabelPrimitive.Root.displayName;
|
|
36
|
+
|
|
37
|
+
export { Label, labelVariants };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../utils/cn";
|
|
5
|
+
|
|
6
|
+
const loadingSpinnerVariants = cva("rounded-full animate-spin border-current", {
|
|
7
|
+
variants: {
|
|
8
|
+
/**
|
|
9
|
+
* Size of the spinner
|
|
10
|
+
*/
|
|
11
|
+
size: {
|
|
12
|
+
sm: "w-4 h-4 border-2",
|
|
13
|
+
default: "w-8 h-8 border-2",
|
|
14
|
+
lg: "w-12 h-12 border-[3px]",
|
|
15
|
+
xl: "w-16 h-16 border-4",
|
|
16
|
+
},
|
|
17
|
+
/**
|
|
18
|
+
* Visual variant
|
|
19
|
+
*/
|
|
20
|
+
variant: {
|
|
21
|
+
/** Primary color spinner (default) */
|
|
22
|
+
default: "border-primary/20 border-t-primary",
|
|
23
|
+
/** Accent color spinner */
|
|
24
|
+
accent: "border-accent/20 border-t-accent",
|
|
25
|
+
/** Muted/subtle spinner */
|
|
26
|
+
muted: "border-muted-foreground/20 border-t-muted-foreground",
|
|
27
|
+
/** Inherits current text color */
|
|
28
|
+
inherit: "border-current/20 border-t-current",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
size: "default",
|
|
33
|
+
variant: "default",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export interface LoadingSpinnerProps
|
|
38
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
39
|
+
VariantProps<typeof loadingSpinnerVariants> {
|
|
40
|
+
/**
|
|
41
|
+
* Optional label shown below the spinner
|
|
42
|
+
*/
|
|
43
|
+
label?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Show animated dots after the label
|
|
46
|
+
*/
|
|
47
|
+
showDots?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* LoadingSpinner Component
|
|
52
|
+
*
|
|
53
|
+
* A circular loading spinner with size and color variants.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // Basic spinner
|
|
57
|
+
* <LoadingSpinner />
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Large spinner with label
|
|
61
|
+
* <LoadingSpinner size="lg" label="Loading..." />
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // Small accent spinner
|
|
65
|
+
* <LoadingSpinner size="sm" variant="accent" />
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Inherit parent text color
|
|
69
|
+
* <LoadingSpinner variant="inherit" />
|
|
70
|
+
*/
|
|
71
|
+
const LoadingSpinner = React.forwardRef<HTMLDivElement, LoadingSpinnerProps>(
|
|
72
|
+
({ className, size, variant, label, showDots = false, ...props }, ref) => {
|
|
73
|
+
const [dots, setDots] = React.useState("");
|
|
74
|
+
|
|
75
|
+
// Animated dots effect
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (!showDots || !label) return;
|
|
78
|
+
|
|
79
|
+
const interval = setInterval(() => {
|
|
80
|
+
setDots((prev) => (prev.length >= 3 ? "" : prev + "."));
|
|
81
|
+
}, 500);
|
|
82
|
+
|
|
83
|
+
return () => clearInterval(interval);
|
|
84
|
+
}, [showDots, label]);
|
|
85
|
+
|
|
86
|
+
if (label) {
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={ref}
|
|
90
|
+
className={cn("flex flex-col items-center gap-3", className)}
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
<div className={loadingSpinnerVariants({ size, variant })} />
|
|
94
|
+
<p className="text-sm text-muted-foreground">
|
|
95
|
+
{label}
|
|
96
|
+
{showDots && <span className="inline-block w-4">{dots}</span>}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
ref={ref}
|
|
105
|
+
className={cn(loadingSpinnerVariants({ size, variant }), className)}
|
|
106
|
+
{...props}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
LoadingSpinner.displayName = "LoadingSpinner";
|
|
112
|
+
|
|
113
|
+
export { LoadingSpinner, loadingSpinnerVariants };
|