@moontra/moonui 2.0.7 → 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.
- package/package.json +26 -19
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/ui/__tests__/accordion.test.tsx +571 -0
- package/src/components/ui/__tests__/alert.test.tsx +484 -0
- package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
- package/src/components/ui/__tests__/avatar.test.tsx +382 -0
- package/src/components/ui/__tests__/badge.test.tsx +364 -0
- package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
- package/src/components/ui/__tests__/button.test.tsx +91 -0
- package/src/components/ui/__tests__/card.test.tsx +104 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
- package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
- package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
- package/src/components/ui/__tests__/command.test.tsx +677 -0
- package/src/components/ui/__tests__/data-table.test.tsx +256 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
- package/src/components/ui/__tests__/dialog.test.tsx +764 -0
- package/src/components/ui/__tests__/input.test.tsx +318 -0
- package/src/components/ui/__tests__/label.test.tsx +103 -0
- package/src/components/ui/__tests__/popover.test.tsx +637 -0
- package/src/components/ui/__tests__/progress.test.tsx +382 -0
- package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
- package/src/components/ui/__tests__/select.test.tsx +705 -0
- package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
- package/src/components/ui/__tests__/slider.test.tsx +493 -0
- package/src/components/ui/__tests__/switch.test.tsx +372 -0
- package/src/components/ui/__tests__/table.test.tsx +824 -0
- package/src/components/ui/__tests__/tabs.test.tsx +887 -0
- package/src/components/ui/__tests__/toast.test.tsx +458 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
- package/src/components/ui/accordion.tsx +63 -0
- package/src/components/ui/alert.tsx +130 -0
- package/src/components/ui/aspect-ratio.tsx +72 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/breadcrumb.tsx +207 -0
- package/src/components/ui/button.stories.tsx +162 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/calendar.tsx +262 -0
- package/src/components/ui/card.stories.tsx +208 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/collapsible.tsx +129 -0
- package/src/components/ui/color-picker.tsx +664 -0
- package/src/components/ui/command.tsx +218 -0
- package/src/components/ui/data-table.stories.tsx +509 -0
- package/src/components/ui/data-table.tsx +623 -0
- package/src/components/ui/date-picker.stories.tsx +321 -0
- package/src/components/ui/date-picker.tsx +603 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +400 -0
- package/src/components/ui/github-stars.tsx +201 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/input.stories.tsx +255 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/locked-component.tsx +215 -0
- package/src/components/ui/moon-logo.tsx +97 -0
- package/src/components/ui/pagination.tsx +116 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/progress.tsx +182 -0
- package/src/components/ui/radio-group.tsx +243 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/simple-editor.tsx +652 -0
- package/src/components/ui/skeleton.tsx +351 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +322 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +46 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/docs/theme-system.md +1194 -0
- package/src/index.tsx +39 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/theme.tsx +398 -0
- package/src/lib/utils.ts +69 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- 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
|
+
}
|