@moontra/moonui 3.4.0 → 4.1.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.d.mts +34 -19
- package/dist/index.d.ts +34 -19
- package/dist/index.global.js +355 -42
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +539 -387
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +505 -354
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.global.js +14 -1
- package/dist/lib/theme.global.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/alert.test.tsx +5 -5
- package/src/components/ui/__tests__/avatar.test.tsx +12 -12
- package/src/components/ui/__tests__/badge.test.tsx +17 -1
- package/src/components/ui/__tests__/label.test.tsx +3 -2
- package/src/components/ui/__tests__/select.test.tsx +24 -2
- package/src/components/ui/__tests__/switch.test.tsx +36 -3
- package/src/components/ui/__tests__/tags-input.test.tsx +163 -0
- package/src/components/ui/alert.tsx +5 -2
- package/src/components/ui/aspect-ratio.tsx +9 -3
- package/src/components/ui/avatar.tsx +18 -6
- package/src/components/ui/badge.tsx +18 -11
- package/src/components/ui/button.tsx +3 -0
- package/src/components/ui/date-picker.tsx +38 -19
- package/src/components/ui/file-upload.tsx +4 -0
- package/src/components/ui/index.ts +22 -0
- package/src/components/ui/input-otp.tsx +5 -0
- package/src/components/ui/input.tsx +7 -2
- package/src/components/ui/label.tsx +6 -3
- package/src/components/ui/select.tsx +59 -23
- package/src/components/ui/simple-editor.tsx +18 -3
- package/src/components/ui/switch.tsx +108 -51
- package/src/components/ui/tags-input.tsx +86 -14
- package/src/components/ui/textarea.tsx +8 -4
|
@@ -157,6 +157,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
157
157
|
placeholder,
|
|
158
158
|
...props
|
|
159
159
|
}, ref) => {
|
|
160
|
+
// Mesaj id'leri `props.id` yoksa "-error"/"-success" gibi SABİT string'e
|
|
161
|
+
// düşüyordu; aynı sayfada iki id'siz Input olduğunda DOM'da yinelenen id
|
|
162
|
+
// oluşuyor ve aria-describedby yanlış elemanı işaret ediyordu.
|
|
163
|
+
const reactId = React.useId();
|
|
164
|
+
const fieldId = props.id || reactId;
|
|
160
165
|
// Floating label state management
|
|
161
166
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
162
167
|
const [internalValue, setInternalValue] = React.useState(value || defaultValue || "");
|
|
@@ -242,7 +247,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
242
247
|
data-error={!!error ? "" : undefined}
|
|
243
248
|
data-success={!!success ? "" : undefined}
|
|
244
249
|
aria-invalid={!!error || !!isError || undefined}
|
|
245
|
-
aria-describedby={error ? `${
|
|
250
|
+
aria-describedby={error ? `${fieldId}-error` : success ? `${fieldId}-success` : undefined}
|
|
246
251
|
value={value}
|
|
247
252
|
defaultValue={defaultValue}
|
|
248
253
|
onChange={handleChange}
|
|
@@ -280,7 +285,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
280
285
|
messageType === "normal" && "text-muted-foreground",
|
|
281
286
|
messageClassName
|
|
282
287
|
)}
|
|
283
|
-
id={error ? `${
|
|
288
|
+
id={error ? `${fieldId}-error` : success ? `${fieldId}-success` : undefined}
|
|
284
289
|
>
|
|
285
290
|
{error || success || ""}
|
|
286
291
|
</p>
|
|
@@ -7,13 +7,16 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
7
7
|
import { cn } from "../../lib/utils"
|
|
8
8
|
|
|
9
9
|
const labelVariants = cva(
|
|
10
|
-
"text-sm font-medium leading-none text-
|
|
10
|
+
"text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:peer-disabled:opacity-60 transition-colors duration-200"
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
+
export interface LabelProps
|
|
14
|
+
extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>,
|
|
15
|
+
VariantProps<typeof labelVariants> {}
|
|
16
|
+
|
|
13
17
|
const Label = React.forwardRef<
|
|
14
18
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
15
|
-
|
|
16
|
-
VariantProps<typeof labelVariants>
|
|
19
|
+
LabelProps
|
|
17
20
|
>(({ className, ...props }, ref) => (
|
|
18
21
|
<LabelPrimitive.Root
|
|
19
22
|
ref={ref}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
5
5
|
import { Check, ChevronDown, ChevronUp, Loader2 } from "lucide-react"
|
|
6
|
+
import { cva } from "class-variance-authority"
|
|
6
7
|
|
|
7
8
|
import { cn } from "../../lib/utils"
|
|
8
9
|
|
|
@@ -24,7 +25,51 @@ const SelectValue = SelectPrimitive.Value
|
|
|
24
25
|
type SelectTriggerVariant = "standard" | "outline" | "ghost" | "underline";
|
|
25
26
|
type SelectTriggerSize = "sm" | "md" | "lg";
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Trigger varyantları `cva()` ile yönetilir (kütüphane geneli desen). Önceden
|
|
30
|
+
* `variant === "x" && "..."` manuel zincirlemesi kullanılıyor ve tüm renkler ham
|
|
31
|
+
* `gray-*` paletiyle yazılıyordu — tema/karanlık mod bu değerleri etkileyemiyordu.
|
|
32
|
+
* Artık semantik token'lara bağlı (`border-input`, `bg-background`, `bg-muted`…).
|
|
33
|
+
*/
|
|
34
|
+
const selectTriggerVariants = cva(
|
|
35
|
+
[
|
|
36
|
+
"flex w-full items-center justify-between gap-1 rounded-md transition-all duration-200",
|
|
37
|
+
"disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
38
|
+
"focus-visible:outline-none text-foreground",
|
|
39
|
+
],
|
|
40
|
+
{
|
|
41
|
+
variants: {
|
|
42
|
+
variant: {
|
|
43
|
+
standard:
|
|
44
|
+
"border border-input bg-background hover:border-ring focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary",
|
|
45
|
+
outline:
|
|
46
|
+
"border border-input bg-transparent hover:border-ring focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary",
|
|
47
|
+
ghost:
|
|
48
|
+
"border-none bg-muted hover:bg-muted/80 focus-visible:ring-2 focus-visible:ring-primary/30",
|
|
49
|
+
underline:
|
|
50
|
+
"border-t-0 border-l-0 border-r-0 border-b border-input rounded-none px-0 bg-transparent hover:border-ring focus-visible:ring-0 focus-visible:border-b-2 focus-visible:border-primary",
|
|
51
|
+
},
|
|
52
|
+
size: {
|
|
53
|
+
sm: "h-8 text-xs px-2",
|
|
54
|
+
md: "h-10 text-sm px-3",
|
|
55
|
+
lg: "h-12 text-base px-4",
|
|
56
|
+
},
|
|
57
|
+
state: {
|
|
58
|
+
none: "",
|
|
59
|
+
error: "border-error focus-visible:ring-error/30 focus-visible:border-error",
|
|
60
|
+
success:
|
|
61
|
+
"border-success focus-visible:ring-success/30 focus-visible:border-success",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultVariants: {
|
|
65
|
+
variant: "standard",
|
|
66
|
+
size: "md",
|
|
67
|
+
state: "none",
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
export interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
|
|
28
73
|
/** Visual variant */
|
|
29
74
|
variant?: SelectTriggerVariant;
|
|
30
75
|
/** Size */
|
|
@@ -44,45 +89,36 @@ interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeof Selec
|
|
|
44
89
|
const SelectTrigger = React.forwardRef<
|
|
45
90
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
46
91
|
SelectTriggerProps
|
|
47
|
-
>(({ className, children, variant
|
|
92
|
+
>(({ className, children, variant, size, error, success, loading, leftIcon, rightIcon, ...props }, ref) => (
|
|
48
93
|
<SelectPrimitive.Trigger
|
|
49
94
|
ref={ref}
|
|
50
95
|
className={cn(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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-800/80 dark:shadow-inner dark:shadow-gray-950/10 dark:text-gray-200 hover:border-gray-400 dark:hover:border-gray-600 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20 focus-visible:border-primary dark:focus-visible:border-primary/80",
|
|
65
|
-
variant === "outline" && "border border-gray-300 dark:border-gray-700 bg-transparent dark:text-gray-200 hover:border-gray-400 dark:hover:border-gray-600 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20 focus-visible:border-primary dark:focus-visible:border-primary/80",
|
|
66
|
-
variant === "ghost" && "border-none bg-gray-100 dark:bg-gray-800 dark:shadow-inner dark:shadow-gray-950/10 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20",
|
|
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 dark:text-gray-200 dark:bg-transparent 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/80",
|
|
96
|
+
selectTriggerVariants({
|
|
97
|
+
variant,
|
|
98
|
+
size,
|
|
99
|
+
state: error ? "error" : success ? "success" : "none",
|
|
100
|
+
}),
|
|
68
101
|
className
|
|
69
102
|
)}
|
|
70
103
|
{...props}
|
|
71
104
|
disabled={props.disabled || loading}
|
|
105
|
+
// `error` görsel olarak işaretleniyordu ama yardımcı teknolojiye hiç
|
|
106
|
+
// duyurulmuyordu; Input/Textarea aynı kavramda `aria-invalid` set ediyor.
|
|
107
|
+
aria-invalid={error ? true : undefined}
|
|
72
108
|
data-error={error ? true : undefined}
|
|
73
109
|
data-success={success ? true : undefined}
|
|
74
110
|
data-loading={loading ? true : undefined}
|
|
75
111
|
>
|
|
76
112
|
<div className="flex items-center flex-1 gap-2">
|
|
77
113
|
{leftIcon && (
|
|
78
|
-
<span className="flex items-center justify-center text-
|
|
114
|
+
<span className="flex items-center justify-center text-muted-foreground">
|
|
79
115
|
{leftIcon}
|
|
80
116
|
</span>
|
|
81
117
|
)}
|
|
82
118
|
{loading ? (
|
|
83
119
|
<div className="flex items-center gap-2">
|
|
84
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground
|
|
85
|
-
<span className="text-muted-foreground
|
|
120
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
|
121
|
+
<span className="text-muted-foreground">Loading...</span>
|
|
86
122
|
</div>
|
|
87
123
|
) : children}
|
|
88
124
|
</div>
|
|
@@ -198,7 +234,7 @@ SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
|
198
234
|
type SelectItemVariant = "default" | "subtle" | "destructive" | "success" | "warning";
|
|
199
235
|
type SelectItemSize = "sm" | "md" | "lg";
|
|
200
236
|
|
|
201
|
-
interface SelectItemProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
|
|
237
|
+
export interface SelectItemProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
|
|
202
238
|
/** Visual variant */
|
|
203
239
|
variant?: SelectItemVariant;
|
|
204
240
|
/** Size */
|
|
@@ -59,6 +59,10 @@ const ToolbarButton = ({ icon, tooltip, active, onClick, disabled }: ToolbarButt
|
|
|
59
59
|
pressed={active}
|
|
60
60
|
onPressedChange={() => onClick()}
|
|
61
61
|
disabled={disabled}
|
|
62
|
+
// Butonun tek içeriği bir ikon; erişilebilir ad olmadan ekran okuyucu
|
|
63
|
+
// yalnızca "button" der. `tooltip` zaten insan-okur etiketi taşıyor.
|
|
64
|
+
// (Tooltip yalnızca fare/odak ile görünür, erişilebilir ad SAĞLAMAZ.)
|
|
65
|
+
aria-label={tooltip}
|
|
62
66
|
className="h-8 w-8 p-0"
|
|
63
67
|
>
|
|
64
68
|
{icon}
|
|
@@ -82,7 +86,10 @@ const ColorPicker = ({ onColorSelect }: { onColorSelect: (color: string) => void
|
|
|
82
86
|
{colors.map(color => (
|
|
83
87
|
<button
|
|
84
88
|
key={color}
|
|
85
|
-
|
|
89
|
+
type="button"
|
|
90
|
+
// Swatch'ın tek içeriği arka plan rengi — erişilebilir ad şart.
|
|
91
|
+
aria-label={`Select color ${color}`}
|
|
92
|
+
className="w-6 h-6 rounded border-2 border-border hover:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
86
93
|
style={{ backgroundColor: color }}
|
|
87
94
|
onClick={() => onColorSelect(color)}
|
|
88
95
|
/>
|
|
@@ -589,10 +596,18 @@ export const SimpleEditor = React.forwardRef<HTMLDivElement, SimpleEditorProps>(
|
|
|
589
596
|
ref={editorRef}
|
|
590
597
|
contentEditable={!disabled}
|
|
591
598
|
suppressContentEditableWarning
|
|
599
|
+
// Düz `div[contentEditable]` yardımcı teknolojiye bir form alanı
|
|
600
|
+
// olarak görünmüyordu: rolü yok, çok satırlı olduğu bilinmiyor ve
|
|
601
|
+
// erişilebilir adı yok.
|
|
602
|
+
role="textbox"
|
|
603
|
+
aria-multiline="true"
|
|
604
|
+
aria-label={placeholder}
|
|
605
|
+
aria-disabled={disabled || undefined}
|
|
592
606
|
className={cn(
|
|
593
607
|
"p-4 outline-none prose prose-sm dark:prose-invert max-w-none overflow-y-auto",
|
|
594
|
-
|
|
595
|
-
|
|
608
|
+
// Token'a bağlandı (önce `bg-slate-50 dark:bg-zinc-900/95` ve
|
|
609
|
+
// `text-gray-900 dark:text-gray-100` hardcode'luydu).
|
|
610
|
+
"bg-background text-foreground",
|
|
596
611
|
"focus:ring-0",
|
|
597
612
|
"[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mb-4",
|
|
598
613
|
"[&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mb-3",
|
|
@@ -2,11 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
|
5
|
+
import { cva } from "class-variance-authority"
|
|
5
6
|
|
|
6
7
|
import { cn } from "../../lib/utils"
|
|
7
8
|
|
|
8
9
|
type SwitchSize = "sm" | "md" | "lg";
|
|
9
|
-
type SwitchVariant = "primary" | "success" | "warning" | "
|
|
10
|
+
type SwitchVariant = "primary" | "success" | "warning" | "destructive" | "secondary";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Boyut ve renk varyantları `cva()` ile yönetilir (kütüphane geneli desen).
|
|
14
|
+
* Önceden `size === "x" && "..."` biçiminde manuel zincirleme kullanılıyordu;
|
|
15
|
+
* `defaultVariants` yoktu ve varsayılanlar destructuring'de gizliydi.
|
|
16
|
+
*/
|
|
17
|
+
const switchVariants = cva(
|
|
18
|
+
[
|
|
19
|
+
"peer relative inline-flex shrink-0 cursor-pointer items-center rounded-full",
|
|
20
|
+
"border-2 border-transparent transition-colors",
|
|
21
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
22
|
+
"focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
23
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
24
|
+
// Kapalı durum token'a bağlı; `--input` karanlık modda kendi değerini taşır.
|
|
25
|
+
// (Önceden ayrıca `dark:data-[state=unchecked]:bg-gray-700/70` hardcode'u
|
|
26
|
+
// vardı ve token'ı eziyordu — kaldırıldı.)
|
|
27
|
+
"data-[state=unchecked]:bg-input",
|
|
28
|
+
"dark:focus-visible:ring-primary/40",
|
|
29
|
+
],
|
|
30
|
+
{
|
|
31
|
+
variants: {
|
|
32
|
+
size: {
|
|
33
|
+
sm: "h-4 w-8",
|
|
34
|
+
md: "h-6 w-11",
|
|
35
|
+
lg: "h-7 w-14",
|
|
36
|
+
},
|
|
37
|
+
variant: {
|
|
38
|
+
primary: "data-[state=checked]:bg-primary",
|
|
39
|
+
success: "data-[state=checked]:bg-success",
|
|
40
|
+
warning: "data-[state=checked]:bg-warning",
|
|
41
|
+
destructive: "data-[state=checked]:bg-error",
|
|
42
|
+
secondary: "data-[state=checked]:bg-accent",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: {
|
|
46
|
+
size: "md",
|
|
47
|
+
variant: "primary",
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const switchThumbVariants = cva(
|
|
53
|
+
"pointer-events-none block rounded-full bg-background shadow-lg ring-0 transition-transform",
|
|
54
|
+
{
|
|
55
|
+
variants: {
|
|
56
|
+
size: {
|
|
57
|
+
sm: "h-3 w-3 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
|
58
|
+
md: "h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
|
59
|
+
lg: "h-6 w-6 data-[state=checked]:translate-x-7 data-[state=unchecked]:translate-x-0",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
defaultVariants: {
|
|
63
|
+
size: "md",
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
)
|
|
10
67
|
|
|
11
68
|
export interface SwitchProps extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> {
|
|
12
69
|
/** Switch boyutu */
|
|
@@ -26,58 +83,58 @@ export interface SwitchProps extends React.ComponentPropsWithoutRef<typeof Switc
|
|
|
26
83
|
const Switch = React.forwardRef<
|
|
27
84
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
|
28
85
|
SwitchProps
|
|
29
|
-
>(({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
className
|
|
55
|
-
)}
|
|
56
|
-
disabled={loading || props.disabled}
|
|
57
|
-
{...props}
|
|
58
|
-
ref={ref}
|
|
59
|
-
>
|
|
60
|
-
{loading ? (
|
|
61
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
62
|
-
<div className="h-3 w-3 animate-spin rounded-full border-2 border-gray-300 border-t-primary"></div>
|
|
63
|
-
</div>
|
|
64
|
-
) : null}
|
|
65
|
-
|
|
66
|
-
<SwitchPrimitives.Thumb
|
|
86
|
+
>(({
|
|
87
|
+
className,
|
|
88
|
+
size,
|
|
89
|
+
variant,
|
|
90
|
+
loading,
|
|
91
|
+
leftIcon,
|
|
92
|
+
rightIcon,
|
|
93
|
+
description,
|
|
94
|
+
"aria-describedby": ariaDescribedBy,
|
|
95
|
+
...props
|
|
96
|
+
}, ref) => {
|
|
97
|
+
// `description` görsel olarak render ediliyordu ama switch ile hiçbir
|
|
98
|
+
// programatik bağı yoktu — ekran okuyucu açıklamayı okuyamıyordu.
|
|
99
|
+
// Stabil bir id üretilip `aria-describedby` ile bağlanır; tüketicinin
|
|
100
|
+
// kendi verdiği `aria-describedby` korunur.
|
|
101
|
+
const reactId = React.useId()
|
|
102
|
+
const descriptionId = description ? `${reactId}-description` : undefined
|
|
103
|
+
const describedBy =
|
|
104
|
+
[ariaDescribedBy, descriptionId].filter(Boolean).join(" ") || undefined
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="moonui-theme inline-flex items-center gap-2">
|
|
108
|
+
{leftIcon && <span className="text-muted-foreground">{leftIcon}</span>}
|
|
109
|
+
<SwitchPrimitives.Root
|
|
67
110
|
className={cn(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
size === "md" && "h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
|
72
|
-
size === "lg" && "h-6 w-6 data-[state=checked]:translate-x-7 data-[state=unchecked]:translate-x-0",
|
|
111
|
+
switchVariants({ size, variant }),
|
|
112
|
+
loading && "opacity-80 cursor-wait",
|
|
113
|
+
className
|
|
73
114
|
)}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
115
|
+
disabled={loading || props.disabled}
|
|
116
|
+
aria-describedby={describedBy}
|
|
117
|
+
{...props}
|
|
118
|
+
ref={ref}
|
|
119
|
+
>
|
|
120
|
+
{loading ? (
|
|
121
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
122
|
+
<div className="h-3 w-3 animate-spin rounded-full border-2 border-muted border-t-primary" />
|
|
123
|
+
</div>
|
|
124
|
+
) : null}
|
|
125
|
+
|
|
126
|
+
<SwitchPrimitives.Thumb className={cn(switchThumbVariants({ size }))} />
|
|
127
|
+
</SwitchPrimitives.Root>
|
|
128
|
+
{rightIcon && <span className="text-muted-foreground">{rightIcon}</span>}
|
|
129
|
+
{description && (
|
|
130
|
+
<span id={descriptionId} className="text-sm text-muted-foreground">
|
|
131
|
+
{description}
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
})
|
|
80
137
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
|
81
138
|
|
|
82
|
-
export { Switch }
|
|
139
|
+
export { Switch, switchVariants }
|
|
83
140
|
export type { SwitchSize, SwitchVariant }
|
|
@@ -34,32 +34,92 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
|
|
|
34
34
|
const [inputValue, setInputValue] = React.useState("");
|
|
35
35
|
const [error, setError] = React.useState("");
|
|
36
36
|
|
|
37
|
+
// Klavye navigasyonu için hem dışa verilen ref hem iç ref gerekiyor:
|
|
38
|
+
// etiketler arasında gezinirken odağı input'a geri döndürebilmeliyiz.
|
|
39
|
+
const innerInputRef = React.useRef<HTMLInputElement>(null);
|
|
40
|
+
React.useImperativeHandle(ref, () => innerInputRef.current as HTMLInputElement);
|
|
41
|
+
|
|
42
|
+
const tagButtonRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
|
|
43
|
+
const errorId = React.useId();
|
|
44
|
+
|
|
45
|
+
const focusTag = (index: number) => {
|
|
46
|
+
if (value.length === 0) return;
|
|
47
|
+
const clamped = Math.max(0, Math.min(value.length - 1, index));
|
|
48
|
+
tagButtonRefs.current[clamped]?.focus();
|
|
49
|
+
};
|
|
50
|
+
|
|
37
51
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
38
52
|
if (e.key === "Enter" || e.key === delimiter) {
|
|
39
53
|
e.preventDefault();
|
|
40
54
|
addTag();
|
|
41
55
|
} else if (e.key === "Backspace" && inputValue === "" && value.length > 0) {
|
|
42
56
|
removeTag(value.length - 1);
|
|
57
|
+
} else if (e.key === "ArrowLeft" && inputValue === "" && value.length > 0) {
|
|
58
|
+
// Boş input'ta sola ok → son etikete geç (hedefli silme için giriş noktası).
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
focusTag(value.length - 1);
|
|
43
61
|
}
|
|
44
62
|
};
|
|
45
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Etiketler arası klavye gezintisi. Önceden etiketlere yalnızca Tab ile
|
|
66
|
+
* ulaşılabiliyor ve hedefli silme yoktu — Backspace her zaman SON etiketi
|
|
67
|
+
* siliyordu, aradaki bir etiketi klavyeyle silmenin yolu yoktu.
|
|
68
|
+
*/
|
|
69
|
+
const handleTagKeyDown =
|
|
70
|
+
(index: number) => (e: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
71
|
+
switch (e.key) {
|
|
72
|
+
case "ArrowLeft":
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
focusTag(index - 1);
|
|
75
|
+
break;
|
|
76
|
+
case "ArrowRight":
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
if (index >= value.length - 1) innerInputRef.current?.focus();
|
|
79
|
+
else focusTag(index + 1);
|
|
80
|
+
break;
|
|
81
|
+
case "Home":
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
focusTag(0);
|
|
84
|
+
break;
|
|
85
|
+
case "End":
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
focusTag(value.length - 1);
|
|
88
|
+
break;
|
|
89
|
+
case "Delete":
|
|
90
|
+
case "Backspace": {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
const remaining = value.length - 1;
|
|
93
|
+
removeTag(index);
|
|
94
|
+
// Silinenin yerine geçen etikete odaklan; liste bittiyse input'a dön.
|
|
95
|
+
requestAnimationFrame(() => {
|
|
96
|
+
if (remaining > 0) focusTag(Math.min(index, remaining - 1));
|
|
97
|
+
else innerInputRef.current?.focus();
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
46
106
|
const addTag = () => {
|
|
47
107
|
const tag = inputValue.trim();
|
|
48
|
-
|
|
108
|
+
|
|
49
109
|
if (!tag) return;
|
|
50
|
-
|
|
110
|
+
|
|
51
111
|
if (!allowDuplicates && value.includes(tag)) {
|
|
52
112
|
setError("This tag already exists");
|
|
53
113
|
setTimeout(() => setError(""), 2000);
|
|
54
114
|
return;
|
|
55
115
|
}
|
|
56
|
-
|
|
116
|
+
|
|
57
117
|
if (maxTags && value.length >= maxTags) {
|
|
58
118
|
setError(`Maximum ${maxTags} tags allowed`);
|
|
59
119
|
setTimeout(() => setError(""), 2000);
|
|
60
120
|
return;
|
|
61
121
|
}
|
|
62
|
-
|
|
122
|
+
|
|
63
123
|
onChange([...value, tag]);
|
|
64
124
|
setInputValue("");
|
|
65
125
|
setError("");
|
|
@@ -75,23 +135,28 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
|
|
|
75
135
|
<div
|
|
76
136
|
className={cn(
|
|
77
137
|
"flex items-center min-h-[2.5rem] w-full flex-wrap gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background",
|
|
78
|
-
|
|
138
|
+
// NOT: burada `dark:bg-zinc-950/50 dark:border-zinc-800` hardcode'u
|
|
139
|
+
// vardı ve aynı satırdaki `bg-background`/`border-input` token'larını
|
|
140
|
+
// karanlık modda eziyordu. Token'lar zaten dark-mode duyarlı.
|
|
79
141
|
"focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
|
|
80
142
|
disabled && "cursor-not-allowed opacity-50",
|
|
81
143
|
className
|
|
82
144
|
)}
|
|
83
145
|
>
|
|
84
146
|
{value.map((tag, index) => (
|
|
85
|
-
<Badge
|
|
86
|
-
key={index}
|
|
87
|
-
variant={variant}
|
|
88
|
-
className="gap-1 pr-1.5"
|
|
89
|
-
>
|
|
147
|
+
<Badge key={index} variant={variant} className="gap-1 pr-1.5">
|
|
90
148
|
<span className="text-xs">{tag}</span>
|
|
91
149
|
{!disabled && (
|
|
92
150
|
<button
|
|
151
|
+
ref={(el) => {
|
|
152
|
+
tagButtonRefs.current[index] = el;
|
|
153
|
+
}}
|
|
93
154
|
type="button"
|
|
155
|
+
// Buton içeriği yalnızca X ikonu — erişilebilir ad olmadan
|
|
156
|
+
// ekran okuyucu hangi etiketin silineceğini söyleyemez.
|
|
157
|
+
aria-label={`Remove ${tag}`}
|
|
94
158
|
onClick={() => removeTag(index)}
|
|
159
|
+
onKeyDown={handleTagKeyDown(index)}
|
|
95
160
|
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
96
161
|
>
|
|
97
162
|
<X className="h-3 w-3 text-muted-foreground hover:text-foreground transition-colors" />
|
|
@@ -100,7 +165,7 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
|
|
|
100
165
|
</Badge>
|
|
101
166
|
))}
|
|
102
167
|
<input
|
|
103
|
-
ref={
|
|
168
|
+
ref={innerInputRef}
|
|
104
169
|
type="text"
|
|
105
170
|
value={inputValue}
|
|
106
171
|
onChange={(e) => setInputValue(e.target.value)}
|
|
@@ -108,9 +173,14 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
|
|
|
108
173
|
onBlur={addTag}
|
|
109
174
|
disabled={disabled}
|
|
110
175
|
placeholder={value.length === 0 ? placeholder : ""}
|
|
176
|
+
// Etiket eklendikçe placeholder kalkıyor; erişilebilir ad olmadan
|
|
177
|
+
// input tamamen adsız kalıyordu. Tüketici kendi aria-label'ını
|
|
178
|
+
// verirse aşağıdaki {...props} yayılımı bunu ezer.
|
|
179
|
+
aria-label="Add tag"
|
|
180
|
+
aria-describedby={error ? errorId : undefined}
|
|
181
|
+
aria-invalid={error ? true : undefined}
|
|
111
182
|
className={cn(
|
|
112
183
|
"flex-1 bg-transparent outline-none placeholder:text-muted-foreground",
|
|
113
|
-
"dark:placeholder:text-zinc-500",
|
|
114
184
|
"min-w-[120px]",
|
|
115
185
|
disabled && "cursor-not-allowed"
|
|
116
186
|
)}
|
|
@@ -118,7 +188,9 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
|
|
|
118
188
|
/>
|
|
119
189
|
</div>
|
|
120
190
|
{error && (
|
|
121
|
-
<p className="mt-1 text-xs text-destructive">
|
|
191
|
+
<p id={errorId} role="status" className="mt-1 text-xs text-destructive">
|
|
192
|
+
{error}
|
|
193
|
+
</p>
|
|
122
194
|
)}
|
|
123
195
|
</div>
|
|
124
196
|
);
|
|
@@ -127,4 +199,4 @@ const TagsInput = React.forwardRef<HTMLInputElement, TagsInputProps>(
|
|
|
127
199
|
|
|
128
200
|
TagsInput.displayName = "TagsInput";
|
|
129
201
|
|
|
130
|
-
export { TagsInput };
|
|
202
|
+
export { TagsInput };
|
|
@@ -99,6 +99,10 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
99
99
|
onChange,
|
|
100
100
|
...props
|
|
101
101
|
}, ref) => {
|
|
102
|
+
// Mesaj id'leri `props.id` yoksa sabit string'e düşüyordu -> aynı sayfada
|
|
103
|
+
// iki id'siz Textarea'da yinelenen DOM id'si oluşuyordu.
|
|
104
|
+
const reactId = React.useId()
|
|
105
|
+
const fieldId = props.id || reactId
|
|
102
106
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
|
103
107
|
const [internalValue, setInternalValue] = React.useState(value || defaultValue || "")
|
|
104
108
|
|
|
@@ -172,8 +176,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
172
176
|
maxLength={maxLength}
|
|
173
177
|
aria-invalid={!!error || undefined}
|
|
174
178
|
aria-describedby={
|
|
175
|
-
errorMessage ? `${
|
|
176
|
-
successMessage ? `${
|
|
179
|
+
errorMessage ? `${fieldId}-error` :
|
|
180
|
+
successMessage ? `${fieldId}-success` :
|
|
177
181
|
undefined
|
|
178
182
|
}
|
|
179
183
|
{...props}
|
|
@@ -197,8 +201,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
197
201
|
messageClassName
|
|
198
202
|
)}
|
|
199
203
|
id={
|
|
200
|
-
errorMessage ? `${
|
|
201
|
-
successMessage ? `${
|
|
204
|
+
errorMessage ? `${fieldId}-error` :
|
|
205
|
+
successMessage ? `${fieldId}-success` :
|
|
202
206
|
undefined
|
|
203
207
|
}
|
|
204
208
|
>
|