@moontra/moonui 2.0.8 → 2.0.9

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 (94) hide show
  1. package/package.json +9 -2
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/ui/__tests__/accordion.test.tsx +571 -0
  6. package/src/components/ui/__tests__/alert.test.tsx +484 -0
  7. package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
  8. package/src/components/ui/__tests__/avatar.test.tsx +382 -0
  9. package/src/components/ui/__tests__/badge.test.tsx +364 -0
  10. package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
  11. package/src/components/ui/__tests__/button.test.tsx +91 -0
  12. package/src/components/ui/__tests__/card.test.tsx +104 -0
  13. package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
  14. package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
  15. package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
  16. package/src/components/ui/__tests__/command.test.tsx +677 -0
  17. package/src/components/ui/__tests__/data-table.test.tsx +256 -0
  18. package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
  19. package/src/components/ui/__tests__/dialog.test.tsx +764 -0
  20. package/src/components/ui/__tests__/input.test.tsx +318 -0
  21. package/src/components/ui/__tests__/label.test.tsx +103 -0
  22. package/src/components/ui/__tests__/popover.test.tsx +637 -0
  23. package/src/components/ui/__tests__/progress.test.tsx +382 -0
  24. package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
  25. package/src/components/ui/__tests__/select.test.tsx +705 -0
  26. package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
  27. package/src/components/ui/__tests__/slider.test.tsx +493 -0
  28. package/src/components/ui/__tests__/switch.test.tsx +372 -0
  29. package/src/components/ui/__tests__/table.test.tsx +824 -0
  30. package/src/components/ui/__tests__/tabs.test.tsx +887 -0
  31. package/src/components/ui/__tests__/toast.test.tsx +458 -0
  32. package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
  33. package/src/components/ui/accordion.tsx +63 -0
  34. package/src/components/ui/alert.tsx +130 -0
  35. package/src/components/ui/aspect-ratio.tsx +72 -0
  36. package/src/components/ui/avatar.tsx +135 -0
  37. package/src/components/ui/badge.tsx +225 -0
  38. package/src/components/ui/breadcrumb.tsx +207 -0
  39. package/src/components/ui/button.stories.tsx +162 -0
  40. package/src/components/ui/button.tsx +221 -0
  41. package/src/components/ui/calendar.tsx +262 -0
  42. package/src/components/ui/card.stories.tsx +208 -0
  43. package/src/components/ui/card.tsx +141 -0
  44. package/src/components/ui/checkbox.tsx +256 -0
  45. package/src/components/ui/collapsible.tsx +129 -0
  46. package/src/components/ui/color-picker.tsx +664 -0
  47. package/src/components/ui/command.tsx +218 -0
  48. package/src/components/ui/data-table.stories.tsx +509 -0
  49. package/src/components/ui/data-table.tsx +623 -0
  50. package/src/components/ui/date-picker.stories.tsx +321 -0
  51. package/src/components/ui/date-picker.tsx +603 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/file-upload.tsx +400 -0
  55. package/src/components/ui/github-stars.tsx +201 -0
  56. package/src/components/ui/index.ts +12 -0
  57. package/src/components/ui/input.stories.tsx +255 -0
  58. package/src/components/ui/input.tsx +219 -0
  59. package/src/components/ui/label.tsx +26 -0
  60. package/src/components/ui/locked-component.tsx +215 -0
  61. package/src/components/ui/moon-logo.tsx +97 -0
  62. package/src/components/ui/pagination.tsx +116 -0
  63. package/src/components/ui/popover.tsx +183 -0
  64. package/src/components/ui/progress.tsx +182 -0
  65. package/src/components/ui/radio-group.tsx +243 -0
  66. package/src/components/ui/select.tsx +273 -0
  67. package/src/components/ui/separator.tsx +140 -0
  68. package/src/components/ui/simple-editor.tsx +652 -0
  69. package/src/components/ui/skeleton.tsx +351 -0
  70. package/src/components/ui/slider.tsx +351 -0
  71. package/src/components/ui/switch.tsx +83 -0
  72. package/src/components/ui/table.tsx +322 -0
  73. package/src/components/ui/tabs.tsx +195 -0
  74. package/src/components/ui/textarea.tsx +46 -0
  75. package/src/components/ui/toast.tsx +313 -0
  76. package/src/components/ui/toggle.tsx +47 -0
  77. package/src/components/ui/tooltip.tsx +152 -0
  78. package/src/docs/theme-system.md +1194 -0
  79. package/src/index.tsx +39 -0
  80. package/src/lib/micro-interactions.ts +255 -0
  81. package/src/lib/theme.tsx +398 -0
  82. package/src/lib/utils.ts +69 -0
  83. package/src/styles/design-system.css +365 -0
  84. package/src/styles/index.css +4 -0
  85. package/src/styles/tailwind.css +6 -0
  86. package/src/styles/tokens.css +453 -0
  87. package/src/use-intersection-observer.tsx +154 -0
  88. package/src/use-local-storage.tsx +71 -0
  89. package/src/use-paddle.ts +138 -0
  90. package/src/use-performance-optimizer.ts +379 -0
  91. package/src/use-pro-access.ts +141 -0
  92. package/src/use-scroll-animation.ts +221 -0
  93. package/src/use-subscription.ts +37 -0
  94. package/src/use-toast.ts +32 -0
@@ -0,0 +1,182 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ /**
9
+ * Progress Bileşeni
10
+ *
11
+ * İlerleme durumunu görsel olarak gösteren, tema sistemiyle tam entegre bir bileşen.
12
+ * Yükleme işlemleri, form gönderimi ve diğer zaman alan işlemleri göstermek için kullanılır.
13
+ */
14
+
15
+ const progressVariants = cva(
16
+ "relative overflow-hidden bg-muted",
17
+ {
18
+ variants: {
19
+ variant: {
20
+ default: "bg-muted",
21
+ primary: "bg-primary/20",
22
+ secondary: "bg-secondary/20",
23
+ success: "bg-success/20",
24
+ warning: "bg-warning/20",
25
+ error: "bg-error/20",
26
+ },
27
+ size: {
28
+ xs: "h-1",
29
+ sm: "h-1.5",
30
+ default: "h-2",
31
+ md: "h-2.5",
32
+ lg: "h-3",
33
+ xl: "h-4",
34
+ },
35
+ radius: {
36
+ none: "rounded-none",
37
+ sm: "rounded-sm",
38
+ default: "rounded-md",
39
+ lg: "rounded-lg",
40
+ full: "rounded-full",
41
+ },
42
+ animation: {
43
+ default: "[&>div]:transition-all [&>div]:duration-500",
44
+ smooth: "[&>div]:transition-all [&>div]:duration-700",
45
+ fast: "[&>div]:transition-all [&>div]:duration-300",
46
+ none: "",
47
+ },
48
+ },
49
+ defaultVariants: {
50
+ variant: "default",
51
+ size: "default",
52
+ radius: "full",
53
+ animation: "default",
54
+ },
55
+ }
56
+ );
57
+
58
+ const progressIndicatorVariants = cva(
59
+ "h-full w-full flex-1",
60
+ {
61
+ variants: {
62
+ variant: {
63
+ default: "bg-foreground",
64
+ primary: "bg-primary",
65
+ secondary: "bg-secondary",
66
+ success: "bg-success",
67
+ warning: "bg-warning",
68
+ error: "bg-error",
69
+ },
70
+ animation: {
71
+ default: "transition-all duration-500",
72
+ smooth: "transition-all duration-700",
73
+ fast: "transition-all duration-300",
74
+ none: "",
75
+ },
76
+ },
77
+ defaultVariants: {
78
+ variant: "primary",
79
+ animation: "default",
80
+ },
81
+ }
82
+ );
83
+
84
+ export interface ProgressProps
85
+ extends React.HTMLAttributes<HTMLDivElement>,
86
+ VariantProps<typeof progressVariants> {
87
+ /**
88
+ * İlerleme çubuğu değeri (0-100)
89
+ */
90
+ value?: number;
91
+ /**
92
+ * İndikatör (dolgu) için renk varyantı
93
+ */
94
+ indicatorVariant?: VariantProps<typeof progressIndicatorVariants>["variant"];
95
+ /**
96
+ * İlerleme durumunu gösteren metin görünürlüğü
97
+ */
98
+ showValueLabel?: boolean;
99
+ /**
100
+ * İlerleme değeri etiketi için format (ör: "30%" veya "3/10")
101
+ */
102
+ valueLabel?: string;
103
+ /**
104
+ * Etiket stili
105
+ */
106
+ labelClassName?: string;
107
+ /**
108
+ * Belirsiz ilerleme animasyonu (indeterminate)
109
+ */
110
+ indeterminate?: boolean;
111
+ /**
112
+ * İlerleme değeri maksimumu (default: 100)
113
+ */
114
+ max?: number;
115
+ }
116
+
117
+ const Progress = React.forwardRef<
118
+ HTMLDivElement,
119
+ ProgressProps
120
+ >(({
121
+ className,
122
+ value = 0,
123
+ variant,
124
+ size,
125
+ radius,
126
+ animation,
127
+ indicatorVariant,
128
+ showValueLabel = false,
129
+ valueLabel,
130
+ labelClassName,
131
+ indeterminate = false,
132
+ max = 100,
133
+ ...props
134
+ }, ref) => {
135
+ // İlerleme değeri 0-100 arasında olmalı
136
+ const normalizedValue = Math.max(0, Math.min(value, max));
137
+ const percentage = max > 0 ? (normalizedValue / max) * 100 : 0;
138
+
139
+ // Etiket içeriği
140
+ const label = valueLabel || `${Math.round(percentage)}%`;
141
+
142
+ return (
143
+ <>
144
+ {showValueLabel && (
145
+ <div className="flex justify-between items-center mb-1">
146
+ <span
147
+ className={cn(
148
+ "text-sm font-medium text-muted-foreground",
149
+ labelClassName
150
+ )}
151
+ >
152
+ {label}
153
+ </span>
154
+ </div>
155
+ )}
156
+ <div
157
+ ref={ref}
158
+ className={cn(progressVariants({ variant, size, radius, animation }), className)}
159
+ role="progressbar"
160
+ aria-valuemin={0}
161
+ aria-valuemax={max}
162
+ aria-valuenow={indeterminate ? undefined : normalizedValue}
163
+ {...props}
164
+ >
165
+ <div
166
+ className={cn(
167
+ progressIndicatorVariants({ variant: indicatorVariant || "primary", animation }),
168
+ indeterminate && "animate-indeterminate-progress"
169
+ )}
170
+ style={indeterminate ? {} : { transform: `translateX(-${100 - percentage}%)` }}
171
+ />
172
+ </div>
173
+ </>
174
+ );
175
+ });
176
+ Progress.displayName = "Progress";
177
+
178
+ // Not: Belirsiz ilerleme animasyonu için CSS keyframe styles.css veya Tailwind config içinde tanımlanmalı
179
+ // @keyframes indeterminate-progress içeren bir tanımlama yapılmalıdır
180
+ // .animate-indeterminate-progress sınıfı da bu animasyonu kullanmalıdır
181
+
182
+ export { Progress, progressVariants };
@@ -0,0 +1,243 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Circle } from "lucide-react";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+
7
+ import { cn } from "../../lib/utils";
8
+
9
+ /**
10
+ * Radio Group Component
11
+ *
12
+ * Accessible, customizable, and fully integrated with the theme system radio button group.
13
+ * Allows users to select a single option from a group of choices.
14
+ */
15
+
16
+ const radioGroupItemVariants = cva(
17
+ "aspect-square border focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: "border-border bg-background text-primary",
22
+ outline: "border-border bg-transparent text-primary",
23
+ filled: "border-primary bg-primary/10 text-primary",
24
+ },
25
+ size: {
26
+ sm: "h-3.5 w-3.5",
27
+ default: "h-4 w-4",
28
+ md: "h-5 w-5",
29
+ lg: "h-6 w-6",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ );
38
+
39
+ export interface RadioGroupProps extends React.HTMLAttributes<HTMLDivElement> {
40
+ /**
41
+ * Radio group value
42
+ */
43
+ value?: string;
44
+ /**
45
+ * Function to call when radio group value changes
46
+ */
47
+ onValueChange?: (value: string) => void;
48
+ /**
49
+ * Radio group disabled state
50
+ */
51
+ disabled?: boolean;
52
+ /**
53
+ * Radio group name
54
+ */
55
+ name?: string;
56
+ }
57
+
58
+ const RadioGroupContext = React.createContext<{
59
+ value?: string;
60
+ onValueChange?: (value: string) => void;
61
+ disabled?: boolean;
62
+ name?: string;
63
+ }>({});
64
+
65
+ const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
66
+ ({ className, value, onValueChange, disabled, name, ...props }, ref) => {
67
+ return (
68
+ <RadioGroupContext.Provider value={{ value, onValueChange, disabled, name }}>
69
+ <div
70
+ ref={ref}
71
+ role="radiogroup"
72
+ className={cn("grid gap-2", className)}
73
+ {...props}
74
+ />
75
+ </RadioGroupContext.Provider>
76
+ );
77
+ }
78
+ );
79
+ RadioGroup.displayName = "RadioGroup";
80
+
81
+ export interface RadioGroupItemProps
82
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
83
+ VariantProps<typeof radioGroupItemVariants> {
84
+ /**
85
+ * Custom indicator component
86
+ */
87
+ indicator?: React.ReactNode;
88
+ /**
89
+ * HTML id for radio button
90
+ */
91
+ id?: string;
92
+ /**
93
+ * Radio button value
94
+ */
95
+ value: string;
96
+ /**
97
+ * Radio button disabled state
98
+ */
99
+ disabled?: boolean;
100
+ }
101
+
102
+ const RadioGroupItem = React.forwardRef<
103
+ HTMLInputElement,
104
+ RadioGroupItemProps
105
+ >(({ className, variant, size, indicator, id, value, disabled, ...props }, ref) => {
106
+ // Get context values from radio group
107
+ const radioGroup = React.useContext(RadioGroupContext);
108
+ const generatedId = React.useId();
109
+ const radioId = id || generatedId;
110
+ const isChecked = radioGroup.value === value;
111
+
112
+ // Call onValueChange function when RadioGroupItem changes
113
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
114
+ if (radioGroup.onValueChange) {
115
+ radioGroup.onValueChange(e.target.value);
116
+ }
117
+
118
+ if (props.onChange) {
119
+ props.onChange(e);
120
+ }
121
+ };
122
+
123
+ return (
124
+ <div className="relative flex items-center">
125
+ <input
126
+ type="radio"
127
+ id={radioId}
128
+ ref={ref}
129
+ value={value}
130
+ checked={isChecked}
131
+ disabled={disabled || radioGroup.disabled}
132
+ name={radioGroup.name}
133
+ onChange={handleChange}
134
+ className="sr-only"
135
+ {...props}
136
+ />
137
+ <label
138
+ htmlFor={radioId}
139
+ className={cn(
140
+ radioGroupItemVariants({ variant, size }),
141
+ "rounded-full",
142
+ "focus-visible:ring-primary/50",
143
+ "relative inline-flex shrink-0 cursor-pointer items-center justify-center overflow-hidden",
144
+ disabled && "cursor-not-allowed opacity-50",
145
+ className
146
+ )}
147
+ >
148
+ <span className={cn(
149
+ "absolute inset-0 pointer-events-none",
150
+ isChecked && "flex items-center justify-center"
151
+ )}>
152
+ {isChecked && (indicator || <Circle className="h-[60%] w-[60%] fill-current text-current" />)}
153
+ </span>
154
+ </label>
155
+ </div>
156
+ );
157
+ });
158
+ RadioGroupItem.displayName = "RadioGroupItem";
159
+
160
+ // Radio Label Component
161
+ interface RadioLabelProps extends React.HTMLAttributes<HTMLLabelElement> {
162
+ /**
163
+ * HTML id for radio button
164
+ */
165
+ htmlFor?: string;
166
+ /**
167
+ * Label content
168
+ */
169
+ children: React.ReactNode;
170
+ /**
171
+ * Disabled state style
172
+ */
173
+ disabled?: boolean;
174
+ }
175
+
176
+ const RadioLabel = React.forwardRef<HTMLLabelElement, RadioLabelProps>(
177
+ ({ className, htmlFor, children, disabled = false, ...props }, ref) => {
178
+ return (
179
+ <label
180
+ ref={ref}
181
+ htmlFor={htmlFor}
182
+ className={cn(
183
+ "text-sm font-medium leading-none ml-2 text-foreground",
184
+ disabled && "cursor-not-allowed opacity-70",
185
+ className
186
+ )}
187
+ {...props}
188
+ >
189
+ {children}
190
+ </label>
191
+ );
192
+ }
193
+ );
194
+ RadioLabel.displayName = "RadioLabel";
195
+
196
+ // Radio Item and Label combination
197
+ interface RadioItemWithLabelProps extends RadioGroupItemProps {
198
+ /**
199
+ * Label content
200
+ */
201
+ label: React.ReactNode;
202
+ /**
203
+ * HTML classes for label
204
+ */
205
+ labelClassName?: string;
206
+ /**
207
+ * HTML id for radio button
208
+ */
209
+ id?: string;
210
+ /**
211
+ * Disabled state
212
+ */
213
+ disabled?: boolean;
214
+ }
215
+
216
+ const RadioItemWithLabel = React.forwardRef<
217
+ HTMLInputElement,
218
+ RadioItemWithLabelProps
219
+ >(({
220
+ id,
221
+ label,
222
+ labelClassName,
223
+ ...radioProps
224
+ }, ref) => {
225
+ const generatedId = React.useId();
226
+ const radioId = id || generatedId;
227
+
228
+ return (
229
+ <div className="flex items-center">
230
+ <RadioGroupItem ref={ref} id={radioId} {...radioProps} />
231
+ <RadioLabel
232
+ htmlFor={radioId}
233
+ disabled={radioProps.disabled}
234
+ className={labelClassName}
235
+ >
236
+ {label}
237
+ </RadioLabel>
238
+ </div>
239
+ );
240
+ });
241
+ RadioItemWithLabel.displayName = "RadioItemWithLabel";
242
+
243
+ export { RadioGroup, RadioGroupItem, RadioLabel, RadioItemWithLabel };
@@ -0,0 +1,273 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SelectPrimitive from "@radix-ui/react-select"
5
+ import { Check, ChevronDown, ChevronUp, Loader2 } from "lucide-react"
6
+
7
+ import { cn } from "../../lib/utils"
8
+
9
+ /**
10
+ * Premium Select Component
11
+ *
12
+ * Advanced dropdown/select component with variants, sizes and accessibility features.
13
+ * Compatible with dark and light mode, providing a modern user experience with smooth animations.
14
+ */
15
+
16
+ // Directly re-exporting the Root component
17
+ const Select = SelectPrimitive.Root
18
+ Select.displayName = "Select"
19
+
20
+ const SelectGroup = SelectPrimitive.Group
21
+
22
+ const SelectValue = SelectPrimitive.Value
23
+
24
+ type SelectTriggerVariant = "standard" | "outline" | "ghost" | "underline";
25
+ type SelectTriggerSize = "sm" | "md" | "lg";
26
+
27
+ interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
28
+ /** Visual variant */
29
+ variant?: SelectTriggerVariant;
30
+ /** Size */
31
+ size?: SelectTriggerSize;
32
+ /** Error state */
33
+ error?: boolean | string;
34
+ /** Success state */
35
+ success?: boolean;
36
+ /** Loading state */
37
+ loading?: boolean;
38
+ /** Icon displayed on the left */
39
+ leftIcon?: React.ReactNode;
40
+ /** Icon displayed on the right (instead of default chevron) */
41
+ rightIcon?: React.ReactNode;
42
+ }
43
+
44
+ const SelectTrigger = React.forwardRef<
45
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
46
+ SelectTriggerProps
47
+ >(({ className, children, variant = "standard", size = "md", error, success, loading, leftIcon, rightIcon, ...props }, ref) => (
48
+ <SelectPrimitive.Trigger
49
+ ref={ref}
50
+ className={cn(
51
+ /* Base styles */
52
+ "flex w-full items-center justify-between gap-1 rounded-md transition-all duration-200",
53
+ "disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
54
+ "focus-visible:outline-none",
55
+ /* Error state */
56
+ error && "border-error focus-visible:ring-error/30 focus-visible:border-error",
57
+ /* Success state */
58
+ success && "border-success focus-visible:ring-success/30 focus-visible:border-success",
59
+ /* Size variants */
60
+ size === "sm" && "h-8 text-xs px-2",
61
+ size === "md" && "h-10 text-sm px-3",
62
+ size === "lg" && "h-12 text-base px-4",
63
+ /* Visual variants */
64
+ variant === "standard" && "border border-gray-300 dark:border-gray-700 bg-background dark:bg-gray-900 hover:border-gray-400 dark:hover:border-gray-600 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/50 focus-visible:border-primary dark:focus-visible:border-primary",
65
+ variant === "outline" && "border border-gray-300 dark:border-gray-700 bg-transparent hover:border-gray-400 dark:hover:border-gray-600 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/50 focus-visible:border-primary dark:focus-visible:border-primary",
66
+ variant === "ghost" && "border-none bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/50",
67
+ variant === "underline" && "border-t-0 border-l-0 border-r-0 border-b border-gray-300 dark:border-gray-600 rounded-none px-0 hover:border-gray-400 dark:hover:border-gray-500 focus-visible:ring-0 focus-visible:border-b-2 focus-visible:border-primary dark:focus-visible:border-primary",
68
+ className
69
+ )}
70
+ {...props}
71
+ disabled={props.disabled || loading}
72
+ data-error={error ? true : undefined}
73
+ data-success={success ? true : undefined}
74
+ data-loading={loading ? true : undefined}
75
+ >
76
+ <div className="flex items-center flex-1 gap-2">
77
+ {leftIcon && (
78
+ <span className="flex items-center justify-center text-gray-500 dark:text-gray-400">
79
+ {leftIcon}
80
+ </span>
81
+ )}
82
+ {loading ? (
83
+ <div className="flex items-center gap-2">
84
+ <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground dark:text-gray-400" />
85
+ <span className="text-muted-foreground dark:text-gray-400">Loading...</span>
86
+ </div>
87
+ ) : children}
88
+ </div>
89
+ {!loading && (
90
+ <SelectPrimitive.Icon asChild>
91
+ {rightIcon ? (
92
+ <span className="opacity-60">{rightIcon}</span>
93
+ ) : (
94
+ <ChevronDown className="h-4 w-4 opacity-60 transition-transform duration-200 ease-out group-data-[state=open]:rotate-180" />
95
+ )}
96
+ </SelectPrimitive.Icon>
97
+ )}
98
+ </SelectPrimitive.Trigger>
99
+ ))
100
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
101
+
102
+ /**
103
+ * Scroll Up Button Component
104
+ */
105
+ const SelectScrollUpButton = React.forwardRef<
106
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
107
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
108
+ >(({ className, ...props }, ref) => (
109
+ <SelectPrimitive.ScrollUpButton
110
+ ref={ref}
111
+ className={cn(
112
+ "flex cursor-default items-center justify-center py-1 text-muted-foreground hover:text-foreground transition-colors",
113
+ className
114
+ )}
115
+ {...props}
116
+ >
117
+ <ChevronUp className="h-4 w-4" />
118
+ </SelectPrimitive.ScrollUpButton>
119
+ ))
120
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
121
+
122
+ /**
123
+ * Scroll Down Button Component
124
+ */
125
+ const SelectScrollDownButton = React.forwardRef<
126
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
127
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
128
+ >(({ className, ...props }, ref) => (
129
+ <SelectPrimitive.ScrollDownButton
130
+ ref={ref}
131
+ className={cn(
132
+ "flex cursor-default items-center justify-center py-1 text-muted-foreground hover:text-foreground transition-colors",
133
+ className
134
+ )}
135
+ {...props}
136
+ >
137
+ <ChevronDown className="h-4 w-4" />
138
+ </SelectPrimitive.ScrollDownButton>
139
+ ))
140
+ SelectScrollDownButton.displayName =
141
+ SelectPrimitive.ScrollDownButton.displayName
142
+
143
+ const SelectContent = React.forwardRef<
144
+ React.ElementRef<typeof SelectPrimitive.Content>,
145
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
146
+ >(({ className, children, position = "item-aligned", ...props }, ref) => (
147
+ <SelectPrimitive.Portal>
148
+ <SelectPrimitive.Content
149
+ ref={ref}
150
+ className={cn(
151
+ ["relative z-50 max-h-96 min-w-[8rem] overflow-hidden",
152
+ "rounded-md border border-gray-200 dark:border-gray-700",
153
+ "bg-white dark:bg-gray-900 text-foreground",
154
+ "shadow-lg dark:shadow-gray-900/20",
155
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
156
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
157
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
158
+ "data-[state=open]:duration-150"],
159
+ position === "popper" &&
160
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
161
+ className
162
+ )}
163
+ position={position}
164
+ {...props}
165
+ >
166
+ <SelectScrollUpButton />
167
+ <SelectPrimitive.Viewport
168
+ className={cn(
169
+ "p-1",
170
+ position === "popper" &&
171
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
172
+ )}
173
+ >
174
+ {children}
175
+ </SelectPrimitive.Viewport>
176
+ <SelectScrollDownButton />
177
+ </SelectPrimitive.Content>
178
+ </SelectPrimitive.Portal>
179
+ ))
180
+ SelectContent.displayName = SelectPrimitive.Content.displayName
181
+
182
+ const SelectLabel = React.forwardRef<
183
+ React.ElementRef<typeof SelectPrimitive.Label>,
184
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
185
+ >(({ className, ...props }, ref) => (
186
+ <SelectPrimitive.Label
187
+ ref={ref}
188
+ className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
189
+ {...props}
190
+ />
191
+ ))
192
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
193
+
194
+ type SelectItemVariant = "default" | "subtle" | "destructive" | "success" | "warning";
195
+ type SelectItemSize = "sm" | "md" | "lg";
196
+
197
+ interface SelectItemProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
198
+ /** Visual variant */
199
+ variant?: SelectItemVariant;
200
+ /** Size */
201
+ size?: SelectItemSize;
202
+ /** Icon displayed on the right */
203
+ rightIcon?: React.ReactNode;
204
+ /** Custom indicator icon (instead of default check) */
205
+ customIndicator?: React.ReactNode;
206
+ }
207
+
208
+ const SelectItem = React.forwardRef<
209
+ React.ElementRef<typeof SelectPrimitive.Item>,
210
+ SelectItemProps
211
+ >(({ className, children, variant = "default", size = "md", rightIcon, customIndicator, ...props }, ref) => (
212
+ <SelectPrimitive.Item
213
+ ref={ref}
214
+ className={cn(
215
+ "relative flex w-full cursor-default select-none items-center rounded-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
216
+
217
+ /* Size variants */
218
+ size === "sm" && "py-1 pl-7 pr-2 text-xs",
219
+ size === "md" && "py-1.5 pl-8 pr-2 text-sm",
220
+ size === "lg" && "py-2 pl-9 pr-3 text-base",
221
+
222
+ /* Color variants */
223
+ variant === "default" && "focus:bg-accent focus:text-accent-foreground dark:focus:bg-gray-700 dark:focus:text-gray-100",
224
+ variant === "subtle" && "focus:bg-gray-100 dark:focus:bg-gray-800 focus:text-foreground dark:focus:text-gray-200",
225
+ variant === "destructive" && "text-error dark:text-red-400 focus:bg-error/10 dark:focus:bg-red-900/20 focus:text-error dark:focus:text-red-300",
226
+ variant === "success" && "text-success dark:text-green-400 focus:bg-success/10 dark:focus:bg-green-900/20 focus:text-success dark:focus:text-green-300",
227
+ variant === "warning" && "text-warning dark:text-yellow-400 focus:bg-warning/10 dark:focus:bg-yellow-900/20 focus:text-warning dark:focus:text-yellow-300",
228
+
229
+ className
230
+ )}
231
+ {...props}
232
+ >
233
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
234
+ <SelectPrimitive.ItemIndicator>
235
+ {customIndicator || <Check className="h-4 w-4" />}
236
+ </SelectPrimitive.ItemIndicator>
237
+ </span>
238
+
239
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
240
+
241
+ {rightIcon && (
242
+ <span className="ml-auto pl-2 opacity-70">
243
+ {rightIcon}
244
+ </span>
245
+ )}
246
+ </SelectPrimitive.Item>
247
+ ))
248
+ SelectItem.displayName = SelectPrimitive.Item.displayName
249
+
250
+ const SelectSeparator = React.forwardRef<
251
+ React.ElementRef<typeof SelectPrimitive.Separator>,
252
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
253
+ >(({ className, ...props }, ref) => (
254
+ <SelectPrimitive.Separator
255
+ ref={ref}
256
+ className={cn("-mx-1 my-1 h-px bg-muted dark:bg-gray-700", className)}
257
+ {...props}
258
+ />
259
+ ))
260
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
261
+
262
+ export {
263
+ Select,
264
+ SelectGroup,
265
+ SelectValue,
266
+ SelectTrigger,
267
+ SelectContent,
268
+ SelectLabel,
269
+ SelectItem,
270
+ SelectSeparator,
271
+ SelectScrollUpButton,
272
+ SelectScrollDownButton,
273
+ }