@quarklab/rad-ui 0.1.4 → 0.2.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/dist/index.js +1141 -14
- package/package.json +37 -28
- package/templates/web/aspect-ratio.tsx +5 -0
- package/templates/web/avatar.tsx +47 -0
- package/templates/web/badge.tsx +35 -0
- package/templates/web/button.tsx +54 -0
- package/templates/web/checkbox.tsx +34 -0
- package/templates/web/field.tsx +291 -0
- package/templates/web/input-group.tsx +209 -0
- package/templates/web/input-otp.tsx +85 -0
- package/templates/web/input.tsx +103 -0
- package/templates/web/kbd.tsx +37 -0
- package/templates/web/label.tsx +23 -0
- package/templates/web/lib/utils.ts +7 -0
- package/templates/web/native-select.tsx +71 -0
- package/templates/web/radio-group.tsx +43 -0
- package/templates/web/separator.tsx +29 -0
- package/templates/web/skeleton.tsx +15 -0
- package/templates/web/slider.tsx +46 -0
- package/templates/web/spinner.tsx +44 -0
- package/templates/web/switch.tsx +32 -0
- package/templates/web/textarea.tsx +28 -0
- package/templates/web/toggle-group.tsx +58 -0
- package/templates/web/toggle.tsx +45 -0
- package/README.md +0 -39
- package/dist/index.css +0 -1
- package/dist/index.d.mts +0 -56
- package/dist/index.d.ts +0 -56
- package/dist/index.mjs +0 -14
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
import { Button } from "./button";
|
|
8
|
+
import { Input } from "./input";
|
|
9
|
+
import { Textarea } from "./textarea";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// InputGroup
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const InputGroup = React.forwardRef<
|
|
16
|
+
HTMLDivElement,
|
|
17
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
18
|
+
>(({ className, ...props }, ref) => (
|
|
19
|
+
<div
|
|
20
|
+
ref={ref}
|
|
21
|
+
data-slot="input-group"
|
|
22
|
+
className={cn(
|
|
23
|
+
"group/input-group flex min-w-0 items-stretch rounded-md border border-input shadow-sm",
|
|
24
|
+
"focus-within:ring-1 focus-within:ring-ring",
|
|
25
|
+
"data-[disabled=true]:opacity-50",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
));
|
|
31
|
+
InputGroup.displayName = "InputGroup";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// InputGroupAddon
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const inputGroupAddonVariants = cva(
|
|
38
|
+
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 group-data-[disabled=true]/input-group:opacity-50",
|
|
39
|
+
{
|
|
40
|
+
variants: {
|
|
41
|
+
align: {
|
|
42
|
+
"inline-start":
|
|
43
|
+
"order-first ps-3 has-[>button]:ms-[-0.45rem] has-[>kbd]:ms-[-0.35rem]",
|
|
44
|
+
"inline-end":
|
|
45
|
+
"order-last pe-3 has-[>button]:me-[-0.45rem] has-[>kbd]:me-[-0.35rem]",
|
|
46
|
+
"block-start":
|
|
47
|
+
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
|
48
|
+
"block-end":
|
|
49
|
+
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
defaultVariants: {
|
|
53
|
+
align: "inline-start",
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export interface InputGroupAddonProps
|
|
59
|
+
extends
|
|
60
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
61
|
+
VariantProps<typeof inputGroupAddonVariants> {}
|
|
62
|
+
|
|
63
|
+
const InputGroupAddon = React.forwardRef<HTMLDivElement, InputGroupAddonProps>(
|
|
64
|
+
({ className, align = "inline-start", ...props }, ref) => (
|
|
65
|
+
<div
|
|
66
|
+
ref={ref}
|
|
67
|
+
data-slot="input-group-addon"
|
|
68
|
+
data-align={align}
|
|
69
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
70
|
+
onClick={(e) => {
|
|
71
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
e.currentTarget.parentElement
|
|
75
|
+
?.querySelector<
|
|
76
|
+
HTMLInputElement | HTMLTextAreaElement
|
|
77
|
+
>("input, textarea")
|
|
78
|
+
?.focus();
|
|
79
|
+
}}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
InputGroupAddon.displayName = "InputGroupAddon";
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// InputGroupButton
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
const inputGroupButtonVariants = cva(
|
|
91
|
+
"text-sm shadow-none flex gap-2 items-center",
|
|
92
|
+
{
|
|
93
|
+
variants: {
|
|
94
|
+
size: {
|
|
95
|
+
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
|
96
|
+
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
|
97
|
+
"icon-xs":
|
|
98
|
+
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
99
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
defaultVariants: {
|
|
103
|
+
size: "xs",
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
export interface InputGroupButtonProps
|
|
109
|
+
extends
|
|
110
|
+
Omit<React.ComponentPropsWithRef<typeof Button>, "size">,
|
|
111
|
+
VariantProps<typeof inputGroupButtonVariants> {}
|
|
112
|
+
|
|
113
|
+
const InputGroupButton = React.forwardRef<
|
|
114
|
+
HTMLButtonElement,
|
|
115
|
+
InputGroupButtonProps
|
|
116
|
+
>(
|
|
117
|
+
(
|
|
118
|
+
{ className, type = "button", variant = "ghost", size = "xs", ...props },
|
|
119
|
+
ref
|
|
120
|
+
) => (
|
|
121
|
+
<Button
|
|
122
|
+
ref={ref}
|
|
123
|
+
type={type}
|
|
124
|
+
variant={variant}
|
|
125
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
InputGroupButton.displayName = "InputGroupButton";
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// InputGroupText
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
const InputGroupText = React.forwardRef<
|
|
137
|
+
HTMLSpanElement,
|
|
138
|
+
React.HTMLAttributes<HTMLSpanElement>
|
|
139
|
+
>(({ className, ...props }, ref) => (
|
|
140
|
+
<span
|
|
141
|
+
ref={ref}
|
|
142
|
+
data-slot="input-group-text"
|
|
143
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
));
|
|
147
|
+
InputGroupText.displayName = "InputGroupText";
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// InputGroupInput
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
type InputGroupInputProps = Omit<
|
|
154
|
+
React.ComponentPropsWithRef<typeof Input>,
|
|
155
|
+
"ref"
|
|
156
|
+
>;
|
|
157
|
+
|
|
158
|
+
const InputGroupInput = React.forwardRef<
|
|
159
|
+
HTMLInputElement,
|
|
160
|
+
InputGroupInputProps
|
|
161
|
+
>(({ className, ...props }, ref) => (
|
|
162
|
+
<Input
|
|
163
|
+
ref={ref}
|
|
164
|
+
data-slot="input-group-control"
|
|
165
|
+
className={cn(
|
|
166
|
+
"border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 rounded-none",
|
|
167
|
+
"group-has-[[data-align=inline-start]]/input-group:rounded-e-md",
|
|
168
|
+
"group-has-[[data-align=inline-end]]/input-group:rounded-s-md",
|
|
169
|
+
className
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
/>
|
|
173
|
+
));
|
|
174
|
+
InputGroupInput.displayName = "InputGroupInput";
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// InputGroupTextarea
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
const InputGroupTextarea = React.forwardRef<
|
|
181
|
+
HTMLTextAreaElement,
|
|
182
|
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
183
|
+
>(({ className, ...props }, ref) => (
|
|
184
|
+
<Textarea
|
|
185
|
+
ref={ref}
|
|
186
|
+
data-slot="input-group-control"
|
|
187
|
+
className={cn(
|
|
188
|
+
"border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 rounded-none min-h-0",
|
|
189
|
+
className
|
|
190
|
+
)}
|
|
191
|
+
{...props}
|
|
192
|
+
/>
|
|
193
|
+
));
|
|
194
|
+
InputGroupTextarea.displayName = "InputGroupTextarea";
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Exports
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
InputGroup,
|
|
202
|
+
InputGroupAddon,
|
|
203
|
+
inputGroupAddonVariants,
|
|
204
|
+
InputGroupButton,
|
|
205
|
+
inputGroupButtonVariants,
|
|
206
|
+
InputGroupText,
|
|
207
|
+
InputGroupInput,
|
|
208
|
+
InputGroupTextarea,
|
|
209
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp";
|
|
5
|
+
import { MinusIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../lib/utils";
|
|
8
|
+
|
|
9
|
+
const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
|
|
10
|
+
|
|
11
|
+
function toPersianDigit(
|
|
12
|
+
str: string | null | undefined
|
|
13
|
+
): string | null | undefined {
|
|
14
|
+
if (!str) return str;
|
|
15
|
+
return str.replace(/\d/g, (d) => PERSIAN_DIGITS[parseInt(d)] ?? d);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const InputOTP = React.forwardRef<
|
|
19
|
+
React.ElementRef<typeof OTPInput>,
|
|
20
|
+
React.ComponentPropsWithoutRef<typeof OTPInput> & {
|
|
21
|
+
containerClassName?: string;
|
|
22
|
+
}
|
|
23
|
+
>(({ className, containerClassName, ...props }, ref) => (
|
|
24
|
+
<div dir="ltr">
|
|
25
|
+
<OTPInput
|
|
26
|
+
ref={ref}
|
|
27
|
+
containerClassName={cn(
|
|
28
|
+
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
|
29
|
+
containerClassName
|
|
30
|
+
)}
|
|
31
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
));
|
|
36
|
+
InputOTP.displayName = "InputOTP";
|
|
37
|
+
|
|
38
|
+
const InputOTPGroup = React.forwardRef<
|
|
39
|
+
React.ElementRef<"div">,
|
|
40
|
+
React.ComponentPropsWithoutRef<"div">
|
|
41
|
+
>(({ className, ...props }, ref) => (
|
|
42
|
+
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
|
43
|
+
));
|
|
44
|
+
InputOTPGroup.displayName = "InputOTPGroup";
|
|
45
|
+
|
|
46
|
+
const InputOTPSlot = React.forwardRef<
|
|
47
|
+
React.ElementRef<"div">,
|
|
48
|
+
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
|
49
|
+
>(({ index, className, ...props }, ref) => {
|
|
50
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
51
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn(
|
|
57
|
+
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all",
|
|
58
|
+
"first:rounded-l-md first:border-l last:rounded-r-md",
|
|
59
|
+
isActive && "z-10 ring-2 ring-ring",
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
>
|
|
64
|
+
{toPersianDigit(char)}
|
|
65
|
+
{hasFakeCaret && (
|
|
66
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
67
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
InputOTPSlot.displayName = "InputOTPSlot";
|
|
74
|
+
|
|
75
|
+
const InputOTPSeparator = React.forwardRef<
|
|
76
|
+
React.ElementRef<"div">,
|
|
77
|
+
React.ComponentPropsWithoutRef<"div">
|
|
78
|
+
>(({ ...props }, ref) => (
|
|
79
|
+
<div ref={ref} role="separator" {...props}>
|
|
80
|
+
<MinusIcon className="h-4 w-4" />
|
|
81
|
+
</div>
|
|
82
|
+
));
|
|
83
|
+
InputOTPSeparator.displayName = "InputOTPSeparator";
|
|
84
|
+
|
|
85
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Upload } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
const inputVariants = cva(
|
|
7
|
+
"flex w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
size: {
|
|
11
|
+
sm: "h-9 px-3 text-sm",
|
|
12
|
+
md: "h-10 px-3 py-2 text-sm",
|
|
13
|
+
lg: "h-11 px-4 text-base",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
size: "md",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export interface InputProps
|
|
23
|
+
extends
|
|
24
|
+
Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
|
|
25
|
+
VariantProps<typeof inputVariants> {}
|
|
26
|
+
|
|
27
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
28
|
+
({ className, size, type, ...props }, ref) => {
|
|
29
|
+
// Custom logic for file input to support Farsi text
|
|
30
|
+
if (type === "file") {
|
|
31
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
32
|
+
const [fileName, setFileName] = React.useState<string>("");
|
|
33
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
34
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
37
|
+
React.useImperativeHandle(ref, () => inputRef.current!);
|
|
38
|
+
|
|
39
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
40
|
+
const file = e.target.files?.[0];
|
|
41
|
+
if (file) {
|
|
42
|
+
setFileName(file.name);
|
|
43
|
+
} else {
|
|
44
|
+
setFileName("");
|
|
45
|
+
}
|
|
46
|
+
props.onChange?.(e);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className={cn(
|
|
52
|
+
inputVariants({ size, className }),
|
|
53
|
+
"flex items-center gap-2 cursor-pointer",
|
|
54
|
+
props.disabled && "cursor-not-allowed opacity-50"
|
|
55
|
+
)}
|
|
56
|
+
onClick={() => !props.disabled && inputRef.current?.click()}
|
|
57
|
+
>
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
disabled={props.disabled}
|
|
61
|
+
className={cn(
|
|
62
|
+
"flex items-center gap-2 rounded-sm bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground transition-colors",
|
|
63
|
+
"hover:bg-secondary/80 focus:outline-none",
|
|
64
|
+
props.disabled && "pointer-events-none"
|
|
65
|
+
)}
|
|
66
|
+
tabIndex={-1}
|
|
67
|
+
>
|
|
68
|
+
<Upload className="h-3 w-3" />
|
|
69
|
+
<span>انتخاب فایل</span>
|
|
70
|
+
</button>
|
|
71
|
+
<span
|
|
72
|
+
className={cn(
|
|
73
|
+
"text-muted-foreground truncate flex-1 text-right text-xs",
|
|
74
|
+
!fileName && "opacity-70"
|
|
75
|
+
)}
|
|
76
|
+
dir="rtl"
|
|
77
|
+
>
|
|
78
|
+
{fileName || props.placeholder || "فایلی انتخاب نشده"}
|
|
79
|
+
</span>
|
|
80
|
+
<input
|
|
81
|
+
type="file"
|
|
82
|
+
className="hidden"
|
|
83
|
+
ref={inputRef}
|
|
84
|
+
onChange={handleFileChange}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<input
|
|
93
|
+
type={type}
|
|
94
|
+
className={cn(inputVariants({ size, className }))}
|
|
95
|
+
ref={ref}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
Input.displayName = "Input";
|
|
102
|
+
|
|
103
|
+
export { Input, inputVariants };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface KbdProps extends React.HTMLAttributes<HTMLElement> {}
|
|
5
|
+
|
|
6
|
+
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
7
|
+
({ className, ...props }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<kbd
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"pointer-events-none inline-flex h-6 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground shadow-sm",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
Kbd.displayName = "Kbd";
|
|
21
|
+
|
|
22
|
+
export interface KbdGroupProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
23
|
+
|
|
24
|
+
const KbdGroup = React.forwardRef<HTMLDivElement, KbdGroupProps>(
|
|
25
|
+
({ className, ...props }, ref) => {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
KbdGroup.displayName = "KbdGroup";
|
|
36
|
+
|
|
37
|
+
export { Kbd, KbdGroup };
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
const labelVariants = cva(
|
|
7
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const Label = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
13
|
+
VariantProps<typeof labelVariants>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<LabelPrimitive.Root
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(labelVariants(), className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
));
|
|
21
|
+
Label.displayName = LabelPrimitive.Root.displayName;
|
|
22
|
+
|
|
23
|
+
export { Label };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
const nativeSelectVariants = cva(
|
|
7
|
+
"flex w-full appearance-none rounded-md border border-input bg-background px-3 py-1 shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 pe-8",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
size: {
|
|
11
|
+
sm: "h-9 text-sm",
|
|
12
|
+
md: "h-10 text-sm",
|
|
13
|
+
lg: "h-11 text-base",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
size: "sm",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export interface NativeSelectProps
|
|
23
|
+
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size">,
|
|
24
|
+
VariantProps<typeof nativeSelectVariants> {}
|
|
25
|
+
|
|
26
|
+
const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
27
|
+
({ className, children, size, ...props }, ref) => {
|
|
28
|
+
return (
|
|
29
|
+
<div className="relative">
|
|
30
|
+
<select
|
|
31
|
+
className={cn(nativeSelectVariants({ size, className }))}
|
|
32
|
+
ref={ref}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</select>
|
|
37
|
+
<ChevronDown className="absolute end-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground pointer-events-none opacity-50" />
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
NativeSelect.displayName = "NativeSelect";
|
|
43
|
+
|
|
44
|
+
export interface NativeSelectOptionProps
|
|
45
|
+
extends React.OptionHTMLAttributes<HTMLOptionElement> {}
|
|
46
|
+
|
|
47
|
+
const NativeSelectOption = React.forwardRef<
|
|
48
|
+
HTMLOptionElement,
|
|
49
|
+
NativeSelectOptionProps
|
|
50
|
+
>(({ className, ...props }, ref) => {
|
|
51
|
+
return <option className={cn("", className)} ref={ref} {...props} />;
|
|
52
|
+
});
|
|
53
|
+
NativeSelectOption.displayName = "NativeSelectOption";
|
|
54
|
+
|
|
55
|
+
export interface NativeSelectOptGroupProps
|
|
56
|
+
extends React.OptgroupHTMLAttributes<HTMLOptGroupElement> {}
|
|
57
|
+
|
|
58
|
+
const NativeSelectOptGroup = React.forwardRef<
|
|
59
|
+
HTMLOptGroupElement,
|
|
60
|
+
NativeSelectOptGroupProps
|
|
61
|
+
>(({ className, ...props }, ref) => {
|
|
62
|
+
return <optgroup className={cn("", className)} ref={ref} {...props} />;
|
|
63
|
+
});
|
|
64
|
+
NativeSelectOptGroup.displayName = "NativeSelectOptGroup";
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
NativeSelect,
|
|
68
|
+
NativeSelectOption,
|
|
69
|
+
NativeSelectOptGroup,
|
|
70
|
+
nativeSelectVariants,
|
|
71
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
3
|
+
import { Circle } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
const RadioGroup = React.forwardRef<
|
|
7
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
8
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
9
|
+
>(({ className, ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<RadioGroupPrimitive.Root
|
|
12
|
+
className={cn("grid gap-2", className)}
|
|
13
|
+
{...props}
|
|
14
|
+
ref={ref}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|
19
|
+
|
|
20
|
+
const RadioGroupItem = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
|
23
|
+
>(({ className, ...props }, ref) => {
|
|
24
|
+
return (
|
|
25
|
+
<RadioGroupPrimitive.Item
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(
|
|
28
|
+
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow transition-colors",
|
|
29
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
30
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
36
|
+
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
|
37
|
+
</RadioGroupPrimitive.Indicator>
|
|
38
|
+
</RadioGroupPrimitive.Item>
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
|
42
|
+
|
|
43
|
+
export { RadioGroup, RadioGroupItem };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
const Separator = React.forwardRef<
|
|
6
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
7
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
8
|
+
>(
|
|
9
|
+
(
|
|
10
|
+
{ className, orientation = "horizontal", decorative = true, ...props },
|
|
11
|
+
ref
|
|
12
|
+
) => (
|
|
13
|
+
<SeparatorPrimitive.Root
|
|
14
|
+
ref={ref}
|
|
15
|
+
decorative={decorative}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"shrink-0 bg-border",
|
|
19
|
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
28
|
+
|
|
29
|
+
export { Separator };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
function Skeleton({
|
|
4
|
+
className,
|
|
5
|
+
...props
|
|
6
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={cn("animate-pulse rounded-md bg-muted", className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { Skeleton };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
const Slider = React.forwardRef<
|
|
6
|
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
7
|
+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
|
8
|
+
>(({ className, value, defaultValue, ...props }, ref) => {
|
|
9
|
+
// Determine the number of thumbs based on value or defaultValue
|
|
10
|
+
const thumbCount = React.useMemo(() => {
|
|
11
|
+
if (value && Array.isArray(value)) {
|
|
12
|
+
return value.length;
|
|
13
|
+
}
|
|
14
|
+
if (defaultValue && Array.isArray(defaultValue)) {
|
|
15
|
+
return defaultValue.length;
|
|
16
|
+
}
|
|
17
|
+
return 1; // Default to single thumb
|
|
18
|
+
}, [value, defaultValue]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<SliderPrimitive.Root
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn(
|
|
24
|
+
"relative flex w-full touch-none select-none items-center",
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
value={value}
|
|
28
|
+
defaultValue={defaultValue}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
|
32
|
+
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
|
33
|
+
</SliderPrimitive.Track>
|
|
34
|
+
{Array.from({ length: thumbCount }).map((_, index) => (
|
|
35
|
+
<SliderPrimitive.Thumb
|
|
36
|
+
key={index}
|
|
37
|
+
className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
38
|
+
/>
|
|
39
|
+
))}
|
|
40
|
+
</SliderPrimitive.Root>
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
45
|
+
|
|
46
|
+
export { Slider };
|