@moontra/moonui 3.0.1 → 3.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/chunk-X5ULQZNC.global.js +33 -0
- package/dist/chunk-X5ULQZNC.global.js.map +1 -0
- package/dist/index.d.mts +169 -2
- package/dist/index.d.ts +169 -2
- package/dist/index.global.js +481 -151
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +989 -288
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +903 -253
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.d.mts +1 -1
- package/dist/lib/theme.d.ts +1 -1
- package/dist/lib/theme.global.js +6 -30
- package/dist/lib/theme.global.js.map +1 -1
- package/dist/lib/theme.js +11 -6
- package/dist/lib/theme.js.map +1 -1
- package/dist/lib/theme.mjs +11 -6
- package/dist/lib/theme.mjs.map +1 -1
- package/package.json +3 -1
- package/src/components/ui/__tests__/carousel.test.tsx +291 -0
- package/src/components/ui/__tests__/input-otp.test.tsx +228 -0
- package/src/components/ui/__tests__/kbd.test.tsx +86 -0
- package/src/components/ui/__tests__/rating.test.tsx +218 -0
- package/src/components/ui/__tests__/spinner.test.tsx +141 -0
- package/src/components/ui/carousel.tsx +324 -0
- package/src/components/ui/index.ts +56 -0
- package/src/components/ui/input-otp.tsx +140 -0
- package/src/components/ui/kbd.tsx +66 -0
- package/src/components/ui/rating.tsx +253 -0
- package/src/components/ui/spinner.tsx +112 -0
- package/tailwind-preset.js +6 -0
- package/dist/chunk-PLK4O6WY.global.js +0 -33
- package/dist/chunk-PLK4O6WY.global.js.map +0 -1
|
@@ -98,6 +98,23 @@ export {
|
|
|
98
98
|
CardZipInput as MoonUICardZipInput,
|
|
99
99
|
} from "./card-input";
|
|
100
100
|
|
|
101
|
+
// Carousel
|
|
102
|
+
export {
|
|
103
|
+
Carousel as MoonUICarousel,
|
|
104
|
+
CarouselContent as MoonUICarouselContent,
|
|
105
|
+
CarouselItem as MoonUICarouselItem,
|
|
106
|
+
CarouselPrevious as MoonUICarouselPrevious,
|
|
107
|
+
CarouselNext as MoonUICarouselNext,
|
|
108
|
+
carouselContentVariants as moonUICarouselContentVariants,
|
|
109
|
+
carouselItemVariants as moonUICarouselItemVariants,
|
|
110
|
+
useCarousel,
|
|
111
|
+
} from "./carousel";
|
|
112
|
+
|
|
113
|
+
export type {
|
|
114
|
+
CarouselProps as MoonUICarouselProps,
|
|
115
|
+
CarouselApi as MoonUICarouselApi,
|
|
116
|
+
} from "./carousel";
|
|
117
|
+
|
|
101
118
|
// Checkbox
|
|
102
119
|
export {
|
|
103
120
|
Checkbox as MoonUICheckbox,
|
|
@@ -220,6 +237,19 @@ export type {
|
|
|
220
237
|
InputProps as MoonUIInputProps,
|
|
221
238
|
} from "./input";
|
|
222
239
|
|
|
240
|
+
// InputOTP
|
|
241
|
+
export {
|
|
242
|
+
InputOTP as MoonUIInputOTP,
|
|
243
|
+
InputOTPGroup as MoonUIInputOTPGroup,
|
|
244
|
+
InputOTPSlot as MoonUIInputOTPSlot,
|
|
245
|
+
InputOTPSeparator as MoonUIInputOTPSeparator,
|
|
246
|
+
inputOTPSlotVariants as moonUIInputOTPSlotVariants,
|
|
247
|
+
} from "./input-otp";
|
|
248
|
+
|
|
249
|
+
export type {
|
|
250
|
+
InputOTPProps as MoonUIInputOTPProps,
|
|
251
|
+
} from "./input-otp";
|
|
252
|
+
|
|
223
253
|
// Label
|
|
224
254
|
export { Label as MoonUILabel } from "./label";
|
|
225
255
|
|
|
@@ -394,6 +424,27 @@ export {
|
|
|
394
424
|
TooltipProvider as MoonUITooltipProvider,
|
|
395
425
|
} from "./tooltip";
|
|
396
426
|
|
|
427
|
+
// Kbd (issue #247) — standalone klavye-tuşu rozeti
|
|
428
|
+
export {
|
|
429
|
+
Kbd as MoonUIKbd,
|
|
430
|
+
kbdVariants as moonUIKbdVariants,
|
|
431
|
+
} from "./kbd";
|
|
432
|
+
export type { KbdProps as MoonUIKbdProps } from "./kbd";
|
|
433
|
+
|
|
434
|
+
// Rating (issue #247) — standalone yıldız değerlendirme
|
|
435
|
+
export {
|
|
436
|
+
Rating as MoonUIRating,
|
|
437
|
+
ratingVariants as moonUIRatingVariants,
|
|
438
|
+
} from "./rating";
|
|
439
|
+
export type { RatingProps as MoonUIRatingProps } from "./rating";
|
|
440
|
+
|
|
441
|
+
// Spinner (issue #247) — standalone yükleme göstergesi
|
|
442
|
+
export {
|
|
443
|
+
Spinner as MoonUISpinner,
|
|
444
|
+
spinnerVariants as moonUISpinnerVariants,
|
|
445
|
+
} from "./spinner";
|
|
446
|
+
export type { SpinnerProps as MoonUISpinnerProps } from "./spinner";
|
|
447
|
+
|
|
397
448
|
// Also export without MoonUI prefix for backward compatibility
|
|
398
449
|
export * from "./accordion";
|
|
399
450
|
export * from "./alert";
|
|
@@ -404,6 +455,7 @@ export * from "./breadcrumb";
|
|
|
404
455
|
export * from "./button";
|
|
405
456
|
export * from "./card";
|
|
406
457
|
export * from "./card-input";
|
|
458
|
+
export * from "./carousel";
|
|
407
459
|
export * from "./checkbox";
|
|
408
460
|
export * from "./collapsible";
|
|
409
461
|
export * from "./color-picker";
|
|
@@ -416,6 +468,7 @@ export * from "./file-upload";
|
|
|
416
468
|
export * from "./gesture-drawer";
|
|
417
469
|
export * from "./github-stars";
|
|
418
470
|
export * from "./input";
|
|
471
|
+
export * from "./input-otp";
|
|
419
472
|
export * from "./label";
|
|
420
473
|
export * from "./locked-component";
|
|
421
474
|
export * from "./moon-logo";
|
|
@@ -441,3 +494,6 @@ export * from "./textarea";
|
|
|
441
494
|
export * from "./toast";
|
|
442
495
|
export * from "./toggle";
|
|
443
496
|
export * from "./tooltip";
|
|
497
|
+
export * from "./kbd";
|
|
498
|
+
export * from "./rating";
|
|
499
|
+
export * from "./spinner";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { Dot } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../lib/utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Premium InputOTP Component
|
|
12
|
+
*
|
|
13
|
+
* input-otp kütüphanesi tabanlı, erişilebilir tek kullanımlık şifre (OTP) girişi.
|
|
14
|
+
* Paste desteği, pattern doğrulama ve aktif slot vurgusu sunar.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Pattern sabitleri — kullanım kolaylığı için input-otp'den yeniden export edilir
|
|
18
|
+
export {
|
|
19
|
+
REGEXP_ONLY_DIGITS,
|
|
20
|
+
REGEXP_ONLY_CHARS,
|
|
21
|
+
REGEXP_ONLY_DIGITS_AND_CHARS,
|
|
22
|
+
} from "input-otp";
|
|
23
|
+
|
|
24
|
+
/* -------------------------------------------------------------------------------------------------
|
|
25
|
+
* InputOTP Root
|
|
26
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
27
|
+
export type InputOTPProps = React.ComponentPropsWithoutRef<typeof OTPInput>;
|
|
28
|
+
|
|
29
|
+
const InputOTP = React.forwardRef<
|
|
30
|
+
React.ElementRef<typeof OTPInput>,
|
|
31
|
+
InputOTPProps
|
|
32
|
+
>(({ className, containerClassName, ...props }, ref) => (
|
|
33
|
+
<OTPInput
|
|
34
|
+
ref={ref}
|
|
35
|
+
containerClassName={cn(
|
|
36
|
+
"moonui-theme",
|
|
37
|
+
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
|
38
|
+
containerClassName
|
|
39
|
+
)}
|
|
40
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
));
|
|
44
|
+
InputOTP.displayName = "InputOTP";
|
|
45
|
+
|
|
46
|
+
/* -------------------------------------------------------------------------------------------------
|
|
47
|
+
* InputOTPGroup
|
|
48
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
49
|
+
const InputOTPGroup = React.forwardRef<
|
|
50
|
+
HTMLDivElement,
|
|
51
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
52
|
+
>(({ className, ...props }, ref) => (
|
|
53
|
+
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
|
54
|
+
));
|
|
55
|
+
InputOTPGroup.displayName = "InputOTPGroup";
|
|
56
|
+
|
|
57
|
+
/* -------------------------------------------------------------------------------------------------
|
|
58
|
+
* InputOTPSlot
|
|
59
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
60
|
+
const inputOTPSlotVariants = cva(
|
|
61
|
+
[
|
|
62
|
+
"relative flex h-10 w-10 items-center justify-center",
|
|
63
|
+
"border-y border-r border-input text-sm text-foreground",
|
|
64
|
+
"transition-all duration-200",
|
|
65
|
+
"first:rounded-l-md first:border-l last:rounded-r-md",
|
|
66
|
+
],
|
|
67
|
+
{
|
|
68
|
+
variants: {
|
|
69
|
+
// Aktif slot vurgusu — token tabanlı ring stili
|
|
70
|
+
isActive: {
|
|
71
|
+
true: "z-10 ring-2 ring-ring ring-offset-background",
|
|
72
|
+
false: "",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultVariants: {
|
|
76
|
+
isActive: false,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export interface InputOTPSlotProps
|
|
82
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
83
|
+
Omit<VariantProps<typeof inputOTPSlotVariants>, "isActive"> {
|
|
84
|
+
/** Bu slotun temsil ettiği karakter index'i */
|
|
85
|
+
index: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const InputOTPSlot = React.forwardRef<HTMLDivElement, InputOTPSlotProps>(
|
|
89
|
+
({ index, className, ...props }, ref) => {
|
|
90
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
91
|
+
const slot = inputOTPContext?.slots?.[index];
|
|
92
|
+
const char = slot?.char;
|
|
93
|
+
const hasFakeCaret = slot?.hasFakeCaret;
|
|
94
|
+
const isActive = slot?.isActive;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
ref={ref}
|
|
99
|
+
data-active={isActive ? "" : undefined}
|
|
100
|
+
className={cn(inputOTPSlotVariants({ isActive: !!isActive }), className)}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{char}
|
|
104
|
+
{hasFakeCaret && (
|
|
105
|
+
// Sahte imleç — gerçek input görünmez olduğu için aktif slotta yanıp söner
|
|
106
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
107
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
InputOTPSlot.displayName = "InputOTPSlot";
|
|
115
|
+
|
|
116
|
+
/* -------------------------------------------------------------------------------------------------
|
|
117
|
+
* InputOTPSeparator
|
|
118
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
119
|
+
const InputOTPSeparator = React.forwardRef<
|
|
120
|
+
HTMLDivElement,
|
|
121
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
122
|
+
>(({ className, ...props }, ref) => (
|
|
123
|
+
<div
|
|
124
|
+
ref={ref}
|
|
125
|
+
role="separator"
|
|
126
|
+
className={cn("text-muted-foreground", className)}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
<Dot aria-hidden="true" />
|
|
130
|
+
</div>
|
|
131
|
+
));
|
|
132
|
+
InputOTPSeparator.displayName = "InputOTPSeparator";
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
InputOTP,
|
|
136
|
+
InputOTPGroup,
|
|
137
|
+
InputOTPSlot,
|
|
138
|
+
InputOTPSeparator,
|
|
139
|
+
inputOTPSlotVariants,
|
|
140
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Premium Kbd Component
|
|
9
|
+
*
|
|
10
|
+
* Klavye tuşlarını (⌘, Ctrl, K vb.) görsel olarak temsil eden standalone
|
|
11
|
+
* bir rozet bileşeni. Dark ve light modda uyumlu, token tabanlı stiller
|
|
12
|
+
* kullanır ve hafif kabartma/gölge ile fiziksel bir tuş görünümü verir.
|
|
13
|
+
*/
|
|
14
|
+
const kbdVariants = cva(
|
|
15
|
+
[
|
|
16
|
+
// Temel klavye-tuşu görünümü — token tabanlı, hardcoded renk yok
|
|
17
|
+
"inline-flex items-center justify-center rounded",
|
|
18
|
+
"border border-border bg-muted",
|
|
19
|
+
"px-1.5 font-mono font-medium text-muted-foreground",
|
|
20
|
+
// Alt kenarda hafif kabartma (border token'ı ile — [#...] hex sınıfı DEĞİL)
|
|
21
|
+
"shadow-[0_1px_0_1px_hsl(var(--border))]",
|
|
22
|
+
"select-none whitespace-nowrap",
|
|
23
|
+
],
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
size: {
|
|
27
|
+
sm: "h-5 min-w-5 text-[10px]",
|
|
28
|
+
md: "h-6 min-w-6 text-xs",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
size: "md",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export interface KbdProps
|
|
38
|
+
extends React.HTMLAttributes<HTMLElement>,
|
|
39
|
+
VariantProps<typeof kbdVariants> {
|
|
40
|
+
/** Tuş içeriği (⌘, K, Ctrl vb.) */
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Premium Kbd Component
|
|
46
|
+
*
|
|
47
|
+
* @param props - Kbd bileşeni özellikleri
|
|
48
|
+
* @param props.size - Tuş boyutu ("sm" | "md")
|
|
49
|
+
* @param props.children - Tuş içeriği (⌘, K, Ctrl vb.)
|
|
50
|
+
*/
|
|
51
|
+
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
52
|
+
({ className, size, children, ...props }, ref) => {
|
|
53
|
+
return (
|
|
54
|
+
<kbd
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn("moonui-theme", kbdVariants({ size }), className)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</kbd>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
Kbd.displayName = "Kbd";
|
|
65
|
+
|
|
66
|
+
export { Kbd, kbdVariants };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { Star } from "lucide-react";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Premium Rating Component
|
|
10
|
+
*
|
|
11
|
+
* Standalone yıldız-değerlendirme bileşeni (input + display).
|
|
12
|
+
* Kontrollü/kontrolsüz kullanım, yarım-yıldız hassasiyeti, salt-gösterim modu
|
|
13
|
+
* ve tam klavye erişilebilirliği (radiogroup) sunar. Dark/light modda uyumludur.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const ratingVariants = cva(
|
|
17
|
+
[
|
|
18
|
+
"moonui-theme inline-flex items-center rounded-md outline-none",
|
|
19
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
20
|
+
],
|
|
21
|
+
{
|
|
22
|
+
variants: {
|
|
23
|
+
size: {
|
|
24
|
+
sm: "gap-0.5",
|
|
25
|
+
md: "gap-1",
|
|
26
|
+
lg: "gap-1.5",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
size: "md",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Boyuta göre ikon ölçüsü (yarım-yıldız maskesinin hizalanması için sabit px gerekir)
|
|
36
|
+
const iconSizeMap: Record<NonNullable<VariantProps<typeof ratingVariants>["size"]>, string> = {
|
|
37
|
+
sm: "h-4 w-4",
|
|
38
|
+
md: "h-5 w-5",
|
|
39
|
+
lg: "h-6 w-6",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface RatingProps
|
|
43
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">,
|
|
44
|
+
VariantProps<typeof ratingVariants> {
|
|
45
|
+
/** Kontrollü değer (0..max) */
|
|
46
|
+
value?: number;
|
|
47
|
+
/** Kontrolsüz başlangıç değeri */
|
|
48
|
+
defaultValue?: number;
|
|
49
|
+
/** Değer değiştiğinde çağrılır */
|
|
50
|
+
onValueChange?: (value: number) => void;
|
|
51
|
+
/** Yıldız sayısı (varsayılan 5) */
|
|
52
|
+
max?: number;
|
|
53
|
+
/** Salt-gösterim: etkileşim yok, aria-readonly işaretlenir */
|
|
54
|
+
readOnly?: boolean;
|
|
55
|
+
/** Yarım-yıldız desteği (varsayılan "full") */
|
|
56
|
+
precision?: "full" | "half";
|
|
57
|
+
/** Özel ikon (varsayılan lucide Star) */
|
|
58
|
+
icon?: React.ReactNode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Premium Rating Component
|
|
63
|
+
*
|
|
64
|
+
* @param props.value - Kontrollü değer
|
|
65
|
+
* @param props.defaultValue - Kontrolsüz başlangıç değeri
|
|
66
|
+
* @param props.onValueChange - Değer değişim geri çağrımı
|
|
67
|
+
* @param props.max - Yıldız sayısı (varsayılan 5)
|
|
68
|
+
* @param props.readOnly - Salt-gösterim modu
|
|
69
|
+
* @param props.precision - "full" | "half" hassasiyet
|
|
70
|
+
* @param props.icon - Özel ikon
|
|
71
|
+
* @param props.size - Boyut (sm | md | lg)
|
|
72
|
+
*/
|
|
73
|
+
const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
|
|
74
|
+
(
|
|
75
|
+
{
|
|
76
|
+
className,
|
|
77
|
+
value,
|
|
78
|
+
defaultValue,
|
|
79
|
+
onValueChange,
|
|
80
|
+
max = 5,
|
|
81
|
+
readOnly = false,
|
|
82
|
+
precision = "full",
|
|
83
|
+
icon,
|
|
84
|
+
size,
|
|
85
|
+
"aria-label": ariaLabel = "Rating",
|
|
86
|
+
...props
|
|
87
|
+
},
|
|
88
|
+
ref
|
|
89
|
+
) => {
|
|
90
|
+
const resolvedSize = size ?? "md";
|
|
91
|
+
const iconSizeClass = iconSizeMap[resolvedSize];
|
|
92
|
+
const step = precision === "half" ? 0.5 : 1;
|
|
93
|
+
|
|
94
|
+
const isControlled = value !== undefined;
|
|
95
|
+
const [internalValue, setInternalValue] = React.useState<number>(defaultValue ?? 0);
|
|
96
|
+
const currentValue = isControlled ? value ?? 0 : internalValue;
|
|
97
|
+
|
|
98
|
+
// Hover önizlemesi (geçici doldurma)
|
|
99
|
+
const [hoverValue, setHoverValue] = React.useState<number | null>(null);
|
|
100
|
+
const displayValue = hoverValue !== null ? hoverValue : currentValue;
|
|
101
|
+
|
|
102
|
+
// Değeri işle: kontrolsüzde iç state'i güncelle, her durumda geri çağrımı tetikle
|
|
103
|
+
const commit = React.useCallback(
|
|
104
|
+
(next: number) => {
|
|
105
|
+
if (readOnly) return;
|
|
106
|
+
if (!isControlled) setInternalValue(next);
|
|
107
|
+
onValueChange?.(next);
|
|
108
|
+
},
|
|
109
|
+
[readOnly, isControlled, onValueChange]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Klavye: ok-tuşları değeri step kadar artır/azalt, Home/End min/max
|
|
113
|
+
const handleKeyDown = React.useCallback(
|
|
114
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
115
|
+
if (readOnly) return;
|
|
116
|
+
let next: number | null = null;
|
|
117
|
+
switch (event.key) {
|
|
118
|
+
case "ArrowRight":
|
|
119
|
+
case "ArrowUp":
|
|
120
|
+
next = Math.min(max, currentValue + step);
|
|
121
|
+
break;
|
|
122
|
+
case "ArrowLeft":
|
|
123
|
+
case "ArrowDown":
|
|
124
|
+
next = Math.max(step, currentValue - step);
|
|
125
|
+
break;
|
|
126
|
+
case "Home":
|
|
127
|
+
next = step;
|
|
128
|
+
break;
|
|
129
|
+
case "End":
|
|
130
|
+
next = max;
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
commit(next);
|
|
137
|
+
},
|
|
138
|
+
[readOnly, max, currentValue, step, commit]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// İkonu doldurmalı/boş olarak üret (özel ikon desteklenir)
|
|
142
|
+
const renderIcon = (filled: boolean): React.ReactNode => {
|
|
143
|
+
if (React.isValidElement(icon)) {
|
|
144
|
+
const element = icon as React.ReactElement<{ className?: string }>;
|
|
145
|
+
return React.cloneElement(element, {
|
|
146
|
+
className: cn(
|
|
147
|
+
iconSizeClass,
|
|
148
|
+
"shrink-0",
|
|
149
|
+
filled && "fill-current",
|
|
150
|
+
element.props?.className
|
|
151
|
+
),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return (
|
|
155
|
+
<Star
|
|
156
|
+
className={cn(iconSizeClass, "shrink-0", filled && "fill-current")}
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const stars = Array.from({ length: max }, (_, index) => index + 1);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
ref={ref}
|
|
167
|
+
role="radiogroup"
|
|
168
|
+
aria-label={ariaLabel}
|
|
169
|
+
aria-orientation="horizontal"
|
|
170
|
+
aria-readonly={readOnly || undefined}
|
|
171
|
+
tabIndex={readOnly ? undefined : 0}
|
|
172
|
+
onKeyDown={handleKeyDown}
|
|
173
|
+
onMouseLeave={() => setHoverValue(null)}
|
|
174
|
+
className={cn(ratingVariants({ size }), className)}
|
|
175
|
+
{...props}
|
|
176
|
+
>
|
|
177
|
+
{stars.map((starValue) => {
|
|
178
|
+
// Bu yıldızın doluluk oranı (0..1) — yarım yıldız için ondalık olur
|
|
179
|
+
const fraction = Math.max(0, Math.min(1, displayValue - (starValue - 1)));
|
|
180
|
+
const halfValue = starValue - 0.5;
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<span
|
|
184
|
+
key={starValue}
|
|
185
|
+
data-slot="rating-star"
|
|
186
|
+
className={cn(
|
|
187
|
+
"relative inline-flex shrink-0",
|
|
188
|
+
!readOnly && "cursor-pointer"
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
{/* Boş katman (görsel taban) */}
|
|
192
|
+
<span aria-hidden="true" className="inline-flex text-muted-foreground/30">
|
|
193
|
+
{renderIcon(false)}
|
|
194
|
+
</span>
|
|
195
|
+
|
|
196
|
+
{/* Dolu katman — overflow-hidden maske ile kısmi doldurma */}
|
|
197
|
+
<span
|
|
198
|
+
aria-hidden="true"
|
|
199
|
+
className="pointer-events-none absolute left-0 top-0 h-full overflow-hidden text-amber-400"
|
|
200
|
+
style={{ width: `${fraction * 100}%` }}
|
|
201
|
+
>
|
|
202
|
+
{renderIcon(true)}
|
|
203
|
+
</span>
|
|
204
|
+
|
|
205
|
+
{/* Etkileşim katmanı (salt-gösterimde yok) */}
|
|
206
|
+
{!readOnly && precision === "half" && (
|
|
207
|
+
<>
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
role="radio"
|
|
211
|
+
aria-checked={currentValue === halfValue}
|
|
212
|
+
aria-label={`${halfValue} stars`}
|
|
213
|
+
tabIndex={-1}
|
|
214
|
+
onClick={() => commit(halfValue)}
|
|
215
|
+
onMouseEnter={() => setHoverValue(halfValue)}
|
|
216
|
+
className="absolute inset-y-0 left-0 z-10 w-1/2 cursor-pointer bg-transparent"
|
|
217
|
+
/>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
role="radio"
|
|
221
|
+
aria-checked={currentValue === starValue}
|
|
222
|
+
aria-label={`${starValue} stars`}
|
|
223
|
+
tabIndex={-1}
|
|
224
|
+
onClick={() => commit(starValue)}
|
|
225
|
+
onMouseEnter={() => setHoverValue(starValue)}
|
|
226
|
+
className="absolute inset-y-0 right-0 z-10 w-1/2 cursor-pointer bg-transparent"
|
|
227
|
+
/>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{!readOnly && precision === "full" && (
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
role="radio"
|
|
235
|
+
aria-checked={currentValue === starValue}
|
|
236
|
+
aria-label={`${starValue} ${starValue === 1 ? "star" : "stars"}`}
|
|
237
|
+
tabIndex={-1}
|
|
238
|
+
onClick={() => commit(starValue)}
|
|
239
|
+
onMouseEnter={() => setHoverValue(starValue)}
|
|
240
|
+
className="absolute inset-0 z-10 cursor-pointer bg-transparent"
|
|
241
|
+
/>
|
|
242
|
+
)}
|
|
243
|
+
</span>
|
|
244
|
+
);
|
|
245
|
+
})}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
Rating.displayName = "Rating";
|
|
252
|
+
|
|
253
|
+
export { Rating, ratingVariants };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Spinner Component
|
|
9
|
+
*
|
|
10
|
+
* Bağımsız (standalone) dönen yükleme göstergesi. Asenkron işlemler
|
|
11
|
+
* sırasında kullanıcıya süregelen bir aktivite olduğunu bildirmek için
|
|
12
|
+
* kullanılır.
|
|
13
|
+
*
|
|
14
|
+
* Erişilebilirlik: `role="status"` + `aria-live="polite"` ile ekran
|
|
15
|
+
* okuyuculara duyurulur; görsel olarak gizli (`sr-only`) `label` metni
|
|
16
|
+
* seslendirilir. `prefers-reduced-motion` açıkken animasyon
|
|
17
|
+
* `motion-reduce:animate-none` ile durdurulur.
|
|
18
|
+
*
|
|
19
|
+
* Renk `currentColor` üzerinden miras alınır; `text-primary`, `text-muted-foreground`
|
|
20
|
+
* gibi metin renk yardımcılarıyla renklendirilir (hardcoded renk yoktur).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Dönen halka (default) varyantının temel stilleri + boyut eşlemesi
|
|
24
|
+
const spinnerVariants = cva(
|
|
25
|
+
[
|
|
26
|
+
"inline-block shrink-0 rounded-full",
|
|
27
|
+
"border-current border-t-transparent",
|
|
28
|
+
"animate-spin motion-reduce:animate-none",
|
|
29
|
+
],
|
|
30
|
+
{
|
|
31
|
+
variants: {
|
|
32
|
+
size: {
|
|
33
|
+
sm: "h-4 w-4 border-2", // 16px
|
|
34
|
+
md: "h-6 w-6 border-2", // 24px
|
|
35
|
+
lg: "h-8 w-8 border-[3px]", // 32px
|
|
36
|
+
xl: "h-10 w-10 border-4", // 40px
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultVariants: {
|
|
40
|
+
size: "md",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Dots varyantı için nokta boyutu eşlemesi (renk `currentColor` ile gelir)
|
|
46
|
+
const dotSizeMap = {
|
|
47
|
+
sm: "h-1 w-1",
|
|
48
|
+
md: "h-1.5 w-1.5",
|
|
49
|
+
lg: "h-2 w-2",
|
|
50
|
+
xl: "h-2.5 w-2.5",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
export interface SpinnerProps
|
|
54
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
55
|
+
VariantProps<typeof spinnerVariants> {
|
|
56
|
+
/**
|
|
57
|
+
* Görsel stil: dönen halka (`default`) veya zıplayan noktalar (`dots`)
|
|
58
|
+
*/
|
|
59
|
+
variant?: "default" | "dots";
|
|
60
|
+
/**
|
|
61
|
+
* Ekran okuyucular için erişilebilir metin (görsel olarak gizli)
|
|
62
|
+
* @default "Loading"
|
|
63
|
+
*/
|
|
64
|
+
label?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Spinner — dönen yükleme göstergesi
|
|
69
|
+
*
|
|
70
|
+
* @param props.size - Gösterge boyutu (`sm` | `md` | `lg` | `xl`)
|
|
71
|
+
* @param props.variant - Görsel stil (`default` halka | `dots` noktalar)
|
|
72
|
+
* @param props.label - Ekran okuyucular için erişilebilir metin (varsayılan "Loading")
|
|
73
|
+
*/
|
|
74
|
+
const Spinner = React.forwardRef<HTMLDivElement, SpinnerProps>(
|
|
75
|
+
({ className, size, variant = "default", label = "Loading", ...props }, ref) => {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
ref={ref}
|
|
79
|
+
role="status"
|
|
80
|
+
aria-live="polite"
|
|
81
|
+
className={cn(
|
|
82
|
+
"moonui-theme inline-flex items-center justify-center",
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
>
|
|
87
|
+
{variant === "dots" ? (
|
|
88
|
+
<span className="inline-flex items-center gap-1" aria-hidden="true">
|
|
89
|
+
{[0, 1, 2].map((i) => (
|
|
90
|
+
<span
|
|
91
|
+
key={i}
|
|
92
|
+
className={cn(
|
|
93
|
+
"inline-block rounded-full bg-current",
|
|
94
|
+
"animate-bounce motion-reduce:animate-none",
|
|
95
|
+
dotSizeMap[size ?? "md"]
|
|
96
|
+
)}
|
|
97
|
+
style={{ animationDelay: `${i * 0.15}s` }}
|
|
98
|
+
/>
|
|
99
|
+
))}
|
|
100
|
+
</span>
|
|
101
|
+
) : (
|
|
102
|
+
<span className={cn(spinnerVariants({ size }))} aria-hidden="true" />
|
|
103
|
+
)}
|
|
104
|
+
{/* Görsel olarak gizli erişilebilir metin */}
|
|
105
|
+
<span className="sr-only">{label}</span>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
Spinner.displayName = "Spinner";
|
|
111
|
+
|
|
112
|
+
export { Spinner, spinnerVariants };
|
package/tailwind-preset.js
CHANGED
|
@@ -133,6 +133,7 @@ module.exports = {
|
|
|
133
133
|
"accordion-up": "accordion-up 0.2s ease-out",
|
|
134
134
|
shake: "shake 0.5s ease-in-out",
|
|
135
135
|
rotate: "rotate 0.5s ease-in-out",
|
|
136
|
+
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
|
136
137
|
},
|
|
137
138
|
keyframes: {
|
|
138
139
|
"accordion-down": {
|
|
@@ -152,6 +153,11 @@ module.exports = {
|
|
|
152
153
|
"0%": { transform: "rotate(0deg)" },
|
|
153
154
|
"100%": { transform: "rotate(360deg)" },
|
|
154
155
|
},
|
|
156
|
+
// InputOTP sahte imleç animasyonu
|
|
157
|
+
"caret-blink": {
|
|
158
|
+
"0%,70%,100%": { opacity: "1" },
|
|
159
|
+
"20%,50%": { opacity: "0" },
|
|
160
|
+
},
|
|
155
161
|
},
|
|
156
162
|
boxShadow: {
|
|
157
163
|
"3xl": "0 35px 60px -15px rgba(0, 0, 0, 0.3)",
|