@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.
@@ -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 };
@@ -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)",