@moontra/moonui 4.0.0 → 5.0.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 +18 -15
- package/dist/index.d.ts +18 -15
- package/dist/index.global.js +29 -29
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +397 -318
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +363 -284
- 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__/badge.test.tsx +5 -2
- package/src/components/ui/__tests__/label.test.tsx +3 -2
- package/src/components/ui/__tests__/tags-input.test.tsx +163 -0
- package/src/components/ui/aspect-ratio.tsx +9 -3
- package/src/components/ui/badge.tsx +4 -4
- 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 +16 -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/simple-editor.tsx +18 -3
- package/src/components/ui/tags-input.tsx +86 -14
- package/src/components/ui/textarea.tsx +8 -4
- package/src/styles/tokens.css +21 -6
|
@@ -17,11 +17,15 @@ const aspectRatioVariants = cva(
|
|
|
17
17
|
"relative overflow-hidden",
|
|
18
18
|
{
|
|
19
19
|
variants: {
|
|
20
|
+
// `variant` yalnız yüzey/kenarlık eksenini yönetir. Önceden her varyant
|
|
21
|
+
// ayrıca `rounded-md` taşıyordu ve `radius` ekseniyle çakışıyordu
|
|
22
|
+
// (aynı anda iki `rounded-*` sınıfı üretiliyordu). Köşe yuvarlaması artık
|
|
23
|
+
// TEK kaynaktan: `radius`.
|
|
20
24
|
variant: {
|
|
21
|
-
default: "
|
|
25
|
+
default: "bg-muted/10",
|
|
22
26
|
ghost: "bg-transparent",
|
|
23
|
-
outline: "
|
|
24
|
-
card: "
|
|
27
|
+
outline: "border border-border",
|
|
28
|
+
card: "bg-card shadow-sm",
|
|
25
29
|
},
|
|
26
30
|
radius: {
|
|
27
31
|
none: "rounded-none",
|
|
@@ -33,6 +37,8 @@ const aspectRatioVariants = cva(
|
|
|
33
37
|
},
|
|
34
38
|
defaultVariants: {
|
|
35
39
|
variant: "default",
|
|
40
|
+
// Önceki davranış korunur: her varyant `rounded-md` taşıyordu.
|
|
41
|
+
radius: "md",
|
|
36
42
|
},
|
|
37
43
|
}
|
|
38
44
|
)
|
|
@@ -51,16 +51,16 @@ const badgeVariants = cva(
|
|
|
51
51
|
"dark:bg-error/90 dark:text-white dark:shadow-inner dark:shadow-error/10",
|
|
52
52
|
],
|
|
53
53
|
success: [
|
|
54
|
-
"border-transparent bg-success text-
|
|
54
|
+
"border-transparent bg-success text-success-foreground",
|
|
55
55
|
"hover:bg-success/90 dark:hover:bg-success/80",
|
|
56
56
|
"focus-visible:ring-success/30 dark:focus-visible:ring-success/25",
|
|
57
|
-
"dark:bg-success/90 dark:text-
|
|
57
|
+
"dark:bg-success/90 dark:text-success-foreground dark:shadow-inner dark:shadow-success/10",
|
|
58
58
|
],
|
|
59
59
|
warning: [
|
|
60
|
-
"border-transparent bg-warning text-
|
|
60
|
+
"border-transparent bg-warning text-warning-foreground",
|
|
61
61
|
"hover:bg-warning/90 dark:hover:bg-warning/80",
|
|
62
62
|
"focus-visible:ring-warning/30 dark:focus-visible:ring-warning/25",
|
|
63
|
-
"dark:bg-warning/90 dark:text-
|
|
63
|
+
"dark:bg-warning/90 dark:text-warning-foreground dark:shadow-inner dark:shadow-warning/10",
|
|
64
64
|
],
|
|
65
65
|
ghost: [
|
|
66
66
|
"border border-gray-200 bg-gray-50/50 text-gray-700",
|
|
@@ -205,6 +205,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
205
205
|
className={cn("moonui-theme", buttonVariants({ variant, size, rounded, fullWidth, className }))}
|
|
206
206
|
ref={ref}
|
|
207
207
|
disabled={disabled || loading}
|
|
208
|
+
// Görsel spinner + data-loading vardı ama yardımcı teknolojiye meşgul
|
|
209
|
+
// durumu hiç duyurulmuyordu.
|
|
210
|
+
aria-busy={loading || undefined}
|
|
208
211
|
data-loading={loading ? "" : undefined}
|
|
209
212
|
{...props}
|
|
210
213
|
>
|
|
@@ -121,7 +121,12 @@ export interface DatePickerProps
|
|
|
121
121
|
calendarProps?: React.ComponentProps<typeof Calendar>;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
// NOT (issue #264): dört seçici de düz fonksiyondu, ref geçme yolu yoktu.
|
|
125
|
+
// Her biri KENDİ gerçek kök DOM elemanına bağlanır — DOM yapısı değişmez.
|
|
126
|
+
// DatePicker/DateTimePicker kök bir <div> render eder; DateRangePicker ve
|
|
127
|
+
// MonthPicker'ın kökü Radix `Popover`'dır ve kendisi DOM üretmez, bu yüzden
|
|
128
|
+
// onlarda ref tetikleyici butona gider.
|
|
129
|
+
export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(function DatePicker({
|
|
125
130
|
className,
|
|
126
131
|
variant,
|
|
127
132
|
size,
|
|
@@ -143,7 +148,7 @@ export function DatePicker({
|
|
|
143
148
|
calendarProps,
|
|
144
149
|
disabled,
|
|
145
150
|
...props
|
|
146
|
-
}: DatePickerProps) {
|
|
151
|
+
}: DatePickerProps, ref: React.ForwardedRef<HTMLDivElement>) {
|
|
147
152
|
const [open, setOpen] = React.useState(false);
|
|
148
153
|
const [internalDate, setInternalDate] = React.useState<Date | undefined>(value);
|
|
149
154
|
|
|
@@ -163,7 +168,11 @@ export function DatePicker({
|
|
|
163
168
|
}
|
|
164
169
|
};
|
|
165
170
|
|
|
166
|
-
|
|
171
|
+
// Hem fare hem klavye (Enter/Space) yolundan çağrılıyor; yalnızca
|
|
172
|
+
// `stopPropagation` kullanıldığı için ortak taban tipi yeterli.
|
|
173
|
+
// (Önceden `React.MouseEvent` bekliyordu ve klavye yolunda `as any` ile
|
|
174
|
+
// zorlanıyordu.)
|
|
175
|
+
const handleClear = (e: React.SyntheticEvent) => {
|
|
167
176
|
e.stopPropagation();
|
|
168
177
|
handleSelect(undefined);
|
|
169
178
|
};
|
|
@@ -188,7 +197,7 @@ export function DatePicker({
|
|
|
188
197
|
const iconElement = icon || <CalendarIcon className="h-4 w-4" />;
|
|
189
198
|
|
|
190
199
|
return (
|
|
191
|
-
<div className="moonui-theme">
|
|
200
|
+
<div ref={ref} className="moonui-theme">
|
|
192
201
|
<Popover open={open} onOpenChange={setOpen}>
|
|
193
202
|
<PopoverTrigger asChild>
|
|
194
203
|
<Button
|
|
@@ -231,7 +240,7 @@ export function DatePicker({
|
|
|
231
240
|
onKeyDown={(e) => {
|
|
232
241
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
233
242
|
e.preventDefault();
|
|
234
|
-
handleClear(e
|
|
243
|
+
handleClear(e);
|
|
235
244
|
}
|
|
236
245
|
}}
|
|
237
246
|
>
|
|
@@ -255,7 +264,7 @@ export function DatePicker({
|
|
|
255
264
|
</Button>
|
|
256
265
|
</PopoverTrigger>
|
|
257
266
|
<PopoverContent
|
|
258
|
-
className="w-[360px] p-0 shadow-2xl rounded-2xl border border-
|
|
267
|
+
className="w-[360px] p-0 shadow-2xl rounded-2xl border border-border bg-background/95 backdrop-blur-sm !z-[9999]"
|
|
259
268
|
align="start"
|
|
260
269
|
sideOffset={12}
|
|
261
270
|
>
|
|
@@ -289,7 +298,8 @@ export function DatePicker({
|
|
|
289
298
|
</Popover>
|
|
290
299
|
</div>
|
|
291
300
|
);
|
|
292
|
-
}
|
|
301
|
+
});
|
|
302
|
+
DatePicker.displayName = "DatePicker";
|
|
293
303
|
|
|
294
304
|
/**
|
|
295
305
|
* DateRangePicker Component
|
|
@@ -313,7 +323,7 @@ export interface DateRangePickerProps
|
|
|
313
323
|
separator?: string;
|
|
314
324
|
}
|
|
315
325
|
|
|
316
|
-
export function DateRangePicker({
|
|
326
|
+
export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(function DateRangePicker({
|
|
317
327
|
className,
|
|
318
328
|
value,
|
|
319
329
|
onChange,
|
|
@@ -321,7 +331,7 @@ export function DateRangePicker({
|
|
|
321
331
|
formatString = "LLL dd, y",
|
|
322
332
|
separator = " - ",
|
|
323
333
|
...props
|
|
324
|
-
}: DateRangePickerProps) {
|
|
334
|
+
}: DateRangePickerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
|
|
325
335
|
const [open, setOpen] = React.useState(false);
|
|
326
336
|
const [internalRange, setInternalRange] = React.useState(value);
|
|
327
337
|
|
|
@@ -352,6 +362,7 @@ export function DateRangePicker({
|
|
|
352
362
|
<Popover open={open} onOpenChange={setOpen}>
|
|
353
363
|
<PopoverTrigger asChild>
|
|
354
364
|
<Button
|
|
365
|
+
ref={ref}
|
|
355
366
|
variant={props.variant === "ghost" ? "ghost" : "outline"}
|
|
356
367
|
className={cn(
|
|
357
368
|
datePickerVariants({
|
|
@@ -370,7 +381,7 @@ export function DateRangePicker({
|
|
|
370
381
|
</span>
|
|
371
382
|
</Button>
|
|
372
383
|
</PopoverTrigger>
|
|
373
|
-
<PopoverContent className="w-auto p-0 shadow-2xl rounded-2xl border border-
|
|
384
|
+
<PopoverContent className="w-auto p-0 shadow-2xl rounded-2xl border border-border bg-background/95 backdrop-blur-sm !z-[9999]" align="start" sideOffset={12}>
|
|
374
385
|
<Calendar
|
|
375
386
|
mode="range"
|
|
376
387
|
defaultMonth={internalRange?.from}
|
|
@@ -383,7 +394,8 @@ export function DateRangePicker({
|
|
|
383
394
|
</PopoverContent>
|
|
384
395
|
</Popover>
|
|
385
396
|
);
|
|
386
|
-
}
|
|
397
|
+
});
|
|
398
|
+
DateRangePicker.displayName = "DateRangePicker";
|
|
387
399
|
|
|
388
400
|
/**
|
|
389
401
|
* DateTimePicker Component
|
|
@@ -406,7 +418,7 @@ export interface DateTimePickerProps extends DatePickerProps {
|
|
|
406
418
|
timeInterval?: number;
|
|
407
419
|
}
|
|
408
420
|
|
|
409
|
-
export function DateTimePicker({
|
|
421
|
+
export const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(function DateTimePicker({
|
|
410
422
|
value,
|
|
411
423
|
onChange,
|
|
412
424
|
formatString = "PPP p",
|
|
@@ -414,7 +426,7 @@ export function DateTimePicker({
|
|
|
414
426
|
timeFormat = "24",
|
|
415
427
|
timeInterval = 15,
|
|
416
428
|
...props
|
|
417
|
-
}: DateTimePickerProps) {
|
|
429
|
+
}: DateTimePickerProps, ref: React.ForwardedRef<HTMLDivElement>) {
|
|
418
430
|
const [date, setDate] = React.useState<Date | undefined>(value);
|
|
419
431
|
const [time, setTime] = React.useState<string>(
|
|
420
432
|
value ? format(value, timeFormat === "24" ? "HH:mm" : "hh:mm a") : "00:00"
|
|
@@ -469,7 +481,7 @@ export function DateTimePicker({
|
|
|
469
481
|
}, [timeFormat, timeInterval]);
|
|
470
482
|
|
|
471
483
|
return (
|
|
472
|
-
<div className="flex flex-col gap-2">
|
|
484
|
+
<div ref={ref} className="flex flex-col gap-2">
|
|
473
485
|
<DatePicker
|
|
474
486
|
{...props}
|
|
475
487
|
value={date}
|
|
@@ -500,7 +512,8 @@ export function DateTimePicker({
|
|
|
500
512
|
)}
|
|
501
513
|
</div>
|
|
502
514
|
);
|
|
503
|
-
}
|
|
515
|
+
});
|
|
516
|
+
DateTimePicker.displayName = "DateTimePicker";
|
|
504
517
|
|
|
505
518
|
/**
|
|
506
519
|
* MonthPicker Component
|
|
@@ -515,13 +528,13 @@ export interface MonthPickerProps extends Omit<DatePickerProps, "formatString">
|
|
|
515
528
|
formatString?: string;
|
|
516
529
|
}
|
|
517
530
|
|
|
518
|
-
export function MonthPicker({
|
|
531
|
+
export const MonthPicker = React.forwardRef<HTMLButtonElement, MonthPickerProps>(function MonthPicker({
|
|
519
532
|
value,
|
|
520
533
|
onChange,
|
|
521
534
|
placeholder = "Pick a month",
|
|
522
535
|
formatString = "MMMM yyyy",
|
|
523
536
|
...props
|
|
524
|
-
}: MonthPickerProps) {
|
|
537
|
+
}: MonthPickerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
|
|
525
538
|
const [open, setOpen] = React.useState(false);
|
|
526
539
|
const [viewDate, setViewDate] = React.useState(value || new Date());
|
|
527
540
|
|
|
@@ -550,6 +563,7 @@ export function MonthPicker({
|
|
|
550
563
|
<Popover open={open} onOpenChange={setOpen}>
|
|
551
564
|
<PopoverTrigger asChild>
|
|
552
565
|
<Button
|
|
566
|
+
ref={ref}
|
|
553
567
|
variant={props.variant === "default" ? "outline" : (props.variant || "outline")}
|
|
554
568
|
className={cn(
|
|
555
569
|
datePickerVariants({
|
|
@@ -566,12 +580,15 @@ export function MonthPicker({
|
|
|
566
580
|
{displayValue || placeholder}
|
|
567
581
|
</Button>
|
|
568
582
|
</PopoverTrigger>
|
|
569
|
-
<PopoverContent className="w-64 p-0 shadow-2xl border border-
|
|
583
|
+
<PopoverContent className="w-64 p-0 shadow-2xl border border-border bg-background !z-[9999]" align="start">
|
|
570
584
|
<div className="p-3">
|
|
571
585
|
<div className="flex items-center justify-between mb-3">
|
|
586
|
+
{/* İkon-only butonlar: erişilebilir ad olmadan ekran okuyucu
|
|
587
|
+
yalnızca "button" der, hangi yöne gidileceği anlaşılmaz. */}
|
|
572
588
|
<Button
|
|
573
589
|
variant="outline"
|
|
574
590
|
size="icon"
|
|
591
|
+
aria-label={`Previous year, ${viewDate.getFullYear() - 1}`}
|
|
575
592
|
onClick={() => handleYearChange(-1)}
|
|
576
593
|
>
|
|
577
594
|
<ChevronLeft className="h-4 w-4" />
|
|
@@ -582,6 +599,7 @@ export function MonthPicker({
|
|
|
582
599
|
<Button
|
|
583
600
|
variant="outline"
|
|
584
601
|
size="icon"
|
|
602
|
+
aria-label={`Next year, ${viewDate.getFullYear() + 1}`}
|
|
585
603
|
onClick={() => handleYearChange(1)}
|
|
586
604
|
>
|
|
587
605
|
<ChevronRight className="h-4 w-4" />
|
|
@@ -604,7 +622,8 @@ export function MonthPicker({
|
|
|
604
622
|
</PopoverContent>
|
|
605
623
|
</Popover>
|
|
606
624
|
);
|
|
607
|
-
}
|
|
625
|
+
});
|
|
626
|
+
MonthPicker.displayName = "MonthPicker";
|
|
608
627
|
|
|
609
628
|
// Re-export utilities
|
|
610
629
|
export { format } from "date-fns";
|
|
@@ -325,6 +325,10 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
|
|
|
325
325
|
multiple={multiple}
|
|
326
326
|
disabled={disabled}
|
|
327
327
|
onChange={handleFileSelect}
|
|
328
|
+
// Input görsel olarak gizli (opacity-0) ve ilişkili bir <label>'ı
|
|
329
|
+
// yok — erişilebilir ad olmadan ekran okuyucu bunu adsız bir dosya
|
|
330
|
+
// alanı olarak duyuruyordu.
|
|
331
|
+
aria-label={multiple ? "Choose files to upload" : "Choose a file to upload"}
|
|
328
332
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
329
333
|
/>
|
|
330
334
|
|
|
@@ -257,6 +257,10 @@ export type {
|
|
|
257
257
|
// Label
|
|
258
258
|
export { Label as MoonUILabel } from "./label";
|
|
259
259
|
|
|
260
|
+
export type {
|
|
261
|
+
LabelProps as MoonUILabelProps,
|
|
262
|
+
} from "./label";
|
|
263
|
+
|
|
260
264
|
// LockedComponent
|
|
261
265
|
export {
|
|
262
266
|
LockedComponent as MoonUILockedComponent,
|
|
@@ -352,6 +356,10 @@ export { Separator as MoonUISeparator } from "./separator";
|
|
|
352
356
|
// SimpleEditor
|
|
353
357
|
export { SimpleEditor as MoonUISimpleEditor } from "./simple-editor";
|
|
354
358
|
|
|
359
|
+
export type {
|
|
360
|
+
SimpleEditorProps as MoonUISimpleEditorProps,
|
|
361
|
+
} from "./simple-editor";
|
|
362
|
+
|
|
355
363
|
// Skeleton
|
|
356
364
|
export {
|
|
357
365
|
Skeleton as MoonUISkeleton,
|
|
@@ -406,6 +414,14 @@ export {
|
|
|
406
414
|
// TagsInput
|
|
407
415
|
export { TagsInput as MoonUITagsInput } from "./tags-input";
|
|
408
416
|
|
|
417
|
+
export type {
|
|
418
|
+
TagsInputProps as MoonUITagsInputProps,
|
|
419
|
+
} from "./tags-input";
|
|
420
|
+
|
|
421
|
+
export type {
|
|
422
|
+
ToggleProps as MoonUIToggleProps,
|
|
423
|
+
} from "./toggle";
|
|
424
|
+
|
|
409
425
|
// Textarea
|
|
410
426
|
export { Textarea as MoonUITextarea } from "./textarea";
|
|
411
427
|
|
|
@@ -32,6 +32,11 @@ const InputOTP = React.forwardRef<
|
|
|
32
32
|
>(({ className, containerClassName, ...props }, ref) => (
|
|
33
33
|
<OTPInput
|
|
34
34
|
ref={ref}
|
|
35
|
+
// input-otp gerçek girişi görsel olarak gizli bir <input> ile yapar ve o
|
|
36
|
+
// input'un ilişkili bir <label>'ı yok — erişilebilir ad olmadan ekran
|
|
37
|
+
// okuyucu adsız bir metin alanı duyuruyordu. Tüketici kendi aria-label'ını
|
|
38
|
+
// verirse aşağıdaki {...props} yayılımı bunu ezer.
|
|
39
|
+
aria-label="One-time password"
|
|
35
40
|
containerClassName={cn(
|
|
36
41
|
"moonui-theme",
|
|
37
42
|
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
|
@@ -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}
|
|
@@ -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",
|
|
@@ -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
|
>
|