@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,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 };