@moontra/moonui 6.7.0 → 6.9.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.
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
4
5
  import { ChevronRight, MoreHorizontal } from "lucide-react";
5
6
  import { cva, type VariantProps } from "class-variance-authority";
6
7
 
@@ -31,10 +32,24 @@ const breadcrumbVariants = cva(
31
32
  interface BreadcrumbProps
32
33
  extends React.HTMLAttributes<HTMLElement>,
33
34
  VariantProps<typeof breadcrumbVariants> {
35
+ /**
36
+ * Alt `BreadcrumbSeparator`'ların varsayılan ayracı. #309: bu prop tipte tanımlıydı ama
37
+ * component gövdesinde HİÇ okunmuyordu → `{...props}` ile `<nav>`'a sızıyor, React
38
+ * bilinmeyen-öznitelik uyarısı basıyordu. Artık context ile alt ayraçlara iniyor;
39
+ * ayracın kendi `children`/`icon` prop'u bunu ezer.
40
+ */
34
41
  separator?: React.ReactNode;
42
+ /**
43
+ * @deprecated Hiçbir zaman uygulanmadı — kaynakta okunmuyordu ve `<nav>`'a sızıyordu
44
+ * (#309'da sızıntı kesildi, davranış eklenmedi). Ev ikonu için ilk `BreadcrumbItem`
45
+ * içine ikonu doğrudan koyun; işaretleme kararı tüketiciye ait olmalı.
46
+ */
35
47
  showHomeIcon?: boolean;
36
48
  }
37
49
 
50
+ /** #309: kök `separator`'ı alt `BreadcrumbSeparator`'lara taşıyan tek kaynak. */
51
+ const BreadcrumbSeparatorContext = React.createContext<React.ReactNode>(undefined);
52
+
38
53
  interface BreadcrumbListProps extends React.HTMLAttributes<HTMLOListElement> {
39
54
  collapsed?: boolean;
40
55
  collapsedWidth?: number;
@@ -57,13 +72,17 @@ interface BreadcrumbEllipsisProps extends React.HTMLAttributes<HTMLSpanElement>
57
72
  }
58
73
 
59
74
  const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>(
60
- ({ className, variant, size, ...props }, ref) => (
61
- <nav
62
- ref={ref}
63
- className={cn("moonui-theme", breadcrumbVariants({ variant, size }), className)}
64
- aria-label="breadcrumb"
65
- {...props}
66
- />
75
+ // #309: `separator` ve `showHomeIcon` ESKİDEN destructure EDİLMİYORDU → `{...props}` ile
76
+ // `<nav>`'a öznitelik olarak basılıyor, React uyarı veriyordu. İkisi de artık ayrılıyor.
77
+ ({ className, variant, size, separator, showHomeIcon: _showHomeIcon, ...props }, ref) => (
78
+ <BreadcrumbSeparatorContext.Provider value={separator}>
79
+ <nav
80
+ ref={ref}
81
+ className={cn("moonui-theme", breadcrumbVariants({ variant, size }), className)}
82
+ aria-label="breadcrumb"
83
+ {...props}
84
+ />
85
+ </BreadcrumbSeparatorContext.Provider>
67
86
  )
68
87
  );
69
88
  Breadcrumb.displayName = "Breadcrumb";
@@ -109,27 +128,34 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, BreadcrumbListProps>(
109
128
  BreadcrumbList.displayName = "BreadcrumbList";
110
129
 
111
130
  const BreadcrumbItem = React.forwardRef<HTMLLIElement, BreadcrumbItemProps>(
112
- ({ className, isCurrent, href, asChild = false, ...props }, ref) => {
113
- const Comp = asChild ? React.Fragment : href ? "a" : "span";
114
- const itemProps = asChild ? {} : href ? { href } : {};
131
+ ({ className, isCurrent, href, asChild = false, children, ...props }, ref) => {
132
+ // #309: `asChild` eskiden `React.Fragment`'e düşüyordu. Fragment yalnız `key` ve
133
+ // `children` kabul eder aşağıdaki `className` sessizce KAYBOLUYOR ve React
134
+ // geliştirme modunda geçersiz-prop uyarısı basıyordu. Radix `Slot` prop'ları tek
135
+ // çocuğa birleştirir (className/style dahil) → hem stil hem a11y korunur.
136
+ const Comp = asChild ? Slot : href ? "a" : "span";
137
+ const itemProps = !asChild && href ? { href } : {};
115
138
 
116
139
  return (
117
140
  <li
118
141
  ref={ref}
119
142
  className={cn("moonui-theme", "inline-flex items-center gap-1.5", className)}
120
- aria-current={isCurrent ? "page" : undefined}
121
143
  {...props}
122
144
  >
123
- <Comp
145
+ <Comp
146
+ // #309: `aria-current` eskiden yalnız sarmalayıcı <li>'deydi; APG breadcrumb
147
+ // deseni onu geçerli sayfayı temsil eden ELEMANDA ister. İkisinde birden
148
+ // bulunması çift duyuruya yol açacağı için <li>'den taşındı (kaldırılmadı).
149
+ aria-current={isCurrent ? "page" : undefined}
124
150
  className={cn(
125
151
  "transition-colors duration-200 hover:text-foreground",
126
- isCurrent
127
- ? "font-medium text-foreground"
152
+ isCurrent
153
+ ? "font-medium text-foreground"
128
154
  : "text-muted-foreground hover:text-foreground hover:underline hover:underline-offset-4 hover:decoration-muted-foreground/30"
129
155
  )}
130
156
  {...itemProps}
131
157
  >
132
- {props.children}
158
+ {children}
133
159
  </Comp>
134
160
  </li>
135
161
  );
@@ -140,30 +166,51 @@ BreadcrumbItem.displayName = "BreadcrumbItem";
140
166
  const BreadcrumbSeparator = ({
141
167
  children,
142
168
  className,
169
+ icon,
143
170
  ...props
144
- }: BreadcrumbSeparatorProps) => (
145
- <span
146
- role="presentation"
147
- aria-hidden="true"
148
- className={cn("moonui-theme", "text-muted-foreground opacity-70", className)}
149
- {...props}
150
- >
151
- {children || <ChevronRight className="h-3.5 w-3.5" />}
152
- </span>
153
- );
171
+ }: BreadcrumbSeparatorProps) => {
172
+ // #309: kök `<Breadcrumb separator={…}>` değeri; yerel `children`/`icon` bunu ezer.
173
+ const rootSeparator = React.useContext(BreadcrumbSeparatorContext);
174
+
175
+ return (
176
+ // Ayraç tümüyle dekoratiftir → `aria-hidden` burada DOĞRU (ellipsis'in aksine içinde
177
+ // ekran-okuyucuya ait metin yok).
178
+ <span
179
+ role="presentation"
180
+ aria-hidden="true"
181
+ className={cn("moonui-theme", "text-muted-foreground opacity-70", className)}
182
+ {...props}
183
+ >
184
+ {/* #309: `icon` prop'u tipte tanımlıydı ama HİÇ OKUNMUYORDU → `{...props}` ile
185
+ DOM'a sızıyor, React bilinmeyen-öznitelik uyarısı basıyordu. Artık `children`
186
+ verilmediğinde ayraç ikonu olarak kullanılıyor. */}
187
+ {children || icon || rootSeparator || <ChevronRight className="h-3.5 w-3.5" />}
188
+ </span>
189
+ );
190
+ };
154
191
  BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
155
192
 
156
193
  const BreadcrumbEllipsis = ({
157
194
  className,
195
+ icon,
158
196
  ...props
159
197
  }: BreadcrumbEllipsisProps) => (
198
+ // #309: `aria-hidden="true"` ESKİDEN BU ELEMANDAYDI → alt ağacın tamamı, içindeki
199
+ // `sr-only` "More pages" metni DAHİL, erişilebilirlik ağacından düşüyordu; yani metin
200
+ // hiçbir zaman duyurulmuyordu (eleman kendi amacını geçersiz kılıyordu). Gizleme artık
201
+ // yalnız dekoratif ikonda.
202
+ //
203
+ // `role="presentation"` KASITLI olarak korundu: span'in zaten örtük rolü yok, dolayısıyla
204
+ // zararsız ve alt ağacı GİZLEMEZ (aria-hidden'ın aksine) — kusur yalnız aria-hidden'dı.
160
205
  <span
161
206
  role="presentation"
162
- aria-hidden="true"
163
207
  className={cn("moonui-theme", "flex items-center text-muted-foreground hover:text-foreground/80 transition-colors duration-200", className)}
164
208
  {...props}
165
209
  >
166
- <MoreHorizontal className="h-4 w-4" />
210
+ <span aria-hidden="true" className="flex items-center">
211
+ {/* #309: `icon` prop'u — ayraçtaki ile aynı ölü-prop kusuru. */}
212
+ {icon || <MoreHorizontal className="h-4 w-4" />}
213
+ </span>
167
214
  <span className="sr-only">More pages</span>
168
215
  </span>
169
216
  );
@@ -206,4 +253,15 @@ export {
206
253
  BreadcrumbPage,
207
254
  BreadcrumbSeparator,
208
255
  BreadcrumbEllipsis,
256
+ breadcrumbVariants,
257
+ };
258
+
259
+ // #309: Props interface'lerinin hiçbiri export edilmiyordu → tüketici sarmalayıcı
260
+ // yazarken tipi yeniden türetmek zorunda kalıyordu.
261
+ export type {
262
+ BreadcrumbProps,
263
+ BreadcrumbListProps,
264
+ BreadcrumbItemProps,
265
+ BreadcrumbSeparatorProps,
266
+ BreadcrumbEllipsisProps,
209
267
  };
@@ -89,12 +89,25 @@ const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
89
89
  );
90
90
  const [canScrollPrev, setCanScrollPrev] = React.useState(false);
91
91
  const [canScrollNext, setCanScrollNext] = React.useState(false);
92
+ /**
93
+ * #308-2: `aria-roledescription="carousel"/"slide"` doğru set edilmişti ama slayt
94
+ * DEĞİŞİMİNİ duyuran hiçbir şey yoktu — klavye/buton ile ilerleyen ekran-okuyucu
95
+ * kullanıcısı hangi slaytta olduğunu öğrenemiyordu. Embla'nın zaten dinlenen "select"
96
+ * olayına bağlı bir `aria-live="polite"` durum bölgesi eklendi.
97
+ */
98
+ const [slideState, setSlideState] = React.useState<{ current: number; total: number }>(
99
+ { current: 0, total: 0 }
100
+ );
92
101
 
93
102
  // Embla "select" olayında ileri/geri butonlarının durumunu güncelle
94
103
  const onSelect = React.useCallback((emblaApi: CarouselApi) => {
95
104
  if (!emblaApi) return;
96
105
  setCanScrollPrev(emblaApi.canScrollPrev());
97
106
  setCanScrollNext(emblaApi.canScrollNext());
107
+ setSlideState({
108
+ current: emblaApi.selectedScrollSnap() + 1,
109
+ total: emblaApi.scrollSnapList().length,
110
+ });
98
111
  }, []);
99
112
 
100
113
  const scrollPrev = React.useCallback(() => {
@@ -164,6 +177,17 @@ const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
164
177
  {...props}
165
178
  >
166
179
  {children}
180
+ {/*
181
+ #308-2: Görsel olarak gizli durum bölgesi. `aria-live="polite"` sayesinde slayt
182
+ değişimi (buton, klavye ya da sürükleme fark etmeksizin) duyurulur; `aria-atomic`
183
+ metnin tamamının okunmasını sağlar. Slayt sayısı henüz bilinmiyorken (ilk render,
184
+ embla init olmadan) hiçbir şey basılmaz — boş duyuru üretmemek için.
185
+ */}
186
+ <div aria-live="polite" aria-atomic="true" className="sr-only">
187
+ {slideState.total > 0
188
+ ? `Slide ${slideState.current} of ${slideState.total}`
189
+ : ""}
190
+ </div>
167
191
  </div>
168
192
  </CarouselContext.Provider>
169
193
  );
@@ -167,6 +167,18 @@ const CommandSeparator = React.forwardRef<
167
167
  >(({ className, ...props }, ref) => (
168
168
  <CommandPrimitive.Separator
169
169
  ref={ref}
170
+ /**
171
+ * #308-6: cmdk, Separator'a `role="separator"` basıyor ve bu eleman
172
+ * `CommandList`'in (`role="listbox"`) çocuğu oluyor → listbox'ın izinli çocuğu
173
+ * yalnız `option`/`group` olduğu için axe `aria-required-children` (critical) veriyordu.
174
+ *
175
+ * `role` EZİLEMİYOR: cmdk 1.1.1 `{...props, "cmdk-separator":"", role:"separator"}`
176
+ * sırasıyla yazıyor, yani kendi rolü prop'lardan SONRA geliyor (kaynak doğrulandı).
177
+ * `aria-hidden` ise prop yayılımının içinde olduğu için geçiyor — ve ayraç zaten
178
+ * tümüyle dekoratif olduğundan erişilebilirlik ağacından çıkarılması DOĞRU davranış.
179
+ * Böylece listbox'ın sahiplendiği çocuklar arasında görünmez.
180
+ */
181
+ aria-hidden="true"
170
182
  className={cn("moonui-theme", "-mx-1 h-px bg-border", className)}
171
183
  {...props}
172
184
  />
@@ -219,4 +231,17 @@ export {
219
231
  CommandItem,
220
232
  CommandShortcut,
221
233
  CommandSeparator,
234
+ }
235
+
236
+ // #308-5: Sekiz Props interface'inin HİÇBİRİ export edilmiyordu → tüketici sarmalayıcı
237
+ // yazarken tipi yeniden türetmek zorundaydı.
238
+ export type {
239
+ CommandProps,
240
+ CommandDialogProps,
241
+ CommandInputProps,
242
+ CommandListProps,
243
+ CommandEmptyProps,
244
+ CommandGroupProps,
245
+ CommandSeparatorProps,
246
+ CommandItemProps,
222
247
  }
@@ -5,13 +5,16 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
5
5
  import { cn } from "../../lib/utils"
6
6
  import { ButtonProps, buttonVariants } from "./button"
7
7
 
8
- const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
9
- <nav
10
- role="navigation"
11
- aria-label="pagination"
12
- className={cn("moonui-theme", "mx-auto flex w-full justify-center", className)}
13
- {...props}
14
- />
8
+ const Pagination = React.forwardRef<HTMLElement, React.ComponentProps<"nav">>(
9
+ ({ className, ...props }, ref) => (
10
+ <nav
11
+ ref={ref}
12
+ role="navigation"
13
+ aria-label="pagination"
14
+ className={cn("moonui-theme", "mx-auto flex w-full justify-center", className)}
15
+ {...props}
16
+ />
17
+ )
15
18
  )
16
19
  Pagination.displayName = "Pagination"
17
20
 
@@ -40,71 +43,85 @@ type PaginationLinkProps = {
40
43
  } & Partial<Pick<ButtonProps, "size">> &
41
44
  React.ComponentProps<"a">
42
45
 
43
- const PaginationLink = ({
44
- className,
45
- isActive,
46
- size = "icon",
47
- ...props
48
- }: PaginationLinkProps) => (
49
- <a
50
- aria-current={isActive ? "page" : undefined}
51
- className={cn(
52
- buttonVariants({
53
- variant: isActive ? "outline" : "ghost",
54
- size,
55
- }),
56
- className
57
- )}
58
- {...props}
59
- />
46
+ const PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
47
+ ({ className, isActive, size = "icon", ...props }, ref) => (
48
+ <a
49
+ ref={ref}
50
+ aria-current={isActive ? "page" : undefined}
51
+ className={cn(
52
+ buttonVariants({
53
+ variant: isActive ? "outline" : "ghost",
54
+ size,
55
+ }),
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ )
60
61
  )
61
62
  PaginationLink.displayName = "PaginationLink"
62
63
 
63
- const PaginationPrevious = ({
64
- className,
65
- ...props
66
- }: React.ComponentProps<typeof PaginationLink>) => (
64
+ /**
65
+ * #315: "Previous"/"Next" etiketleri KOŞULSUZ render ediliyordu ve component'te hiçbir
66
+ * responsive affordance yoktu. 375px'te ölçüm: nav clientWidth=229 / scrollWidth=317
67
+ * (88px iç taşma) ata zincirindeki `overflow-hidden` etiketi kırpıyor, kullanıcı
68
+ * "Previous" yerine "vious" görüyordu. Etiket artık `sm` altında gizleniyor; erişilebilir
69
+ * ad `aria-label` ile korunduğu için ekran-okuyucu deneyimi DEĞİŞMİYOR.
70
+ */
71
+ const paginationLabelClass = "hidden sm:inline"
72
+
73
+ const PaginationPrevious = React.forwardRef<
74
+ HTMLAnchorElement,
75
+ React.ComponentProps<typeof PaginationLink>
76
+ >(({ className, ...props }, ref) => (
67
77
  <PaginationLink
78
+ ref={ref}
68
79
  aria-label="Go to previous page"
69
80
  size="md"
70
81
  className={cn("moonui-theme", "gap-1 pl-2.5", className)}
71
82
  {...props}
72
83
  >
73
84
  <ChevronLeft className="h-4 w-4" />
74
- <span>Previous</span>
85
+ <span className={paginationLabelClass}>Previous</span>
75
86
  </PaginationLink>
76
- )
87
+ ))
77
88
  PaginationPrevious.displayName = "PaginationPrevious"
78
89
 
79
- const PaginationNext = ({
80
- className,
81
- ...props
82
- }: React.ComponentProps<typeof PaginationLink>) => (
90
+ const PaginationNext = React.forwardRef<
91
+ HTMLAnchorElement,
92
+ React.ComponentProps<typeof PaginationLink>
93
+ >(({ className, ...props }, ref) => (
83
94
  <PaginationLink
95
+ ref={ref}
84
96
  aria-label="Go to next page"
85
97
  size="md"
86
98
  className={cn("moonui-theme", "gap-1 pr-2.5", className)}
87
99
  {...props}
88
100
  >
89
- <span>Next</span>
101
+ <span className={paginationLabelClass}>Next</span>
90
102
  <ChevronRight className="h-4 w-4" />
91
103
  </PaginationLink>
92
- )
104
+ ))
93
105
  PaginationNext.displayName = "PaginationNext"
94
106
 
95
- const PaginationEllipsis = ({
96
- className,
97
- ...props
98
- }: React.ComponentProps<"span">) => (
107
+ const PaginationEllipsis = React.forwardRef<
108
+ HTMLSpanElement,
109
+ React.ComponentProps<"span">
110
+ >(({ className, ...props }, ref) => (
111
+ // #315: `aria-hidden` ESKİDEN BU ELEMANDAYDI → içindeki `sr-only` "More pages" metni
112
+ // erişilebilirlik ağacından düşüyor, hiç duyurulmuyordu. Gizleme artık yalnız dekoratif
113
+ // ikonda. (breadcrumb.tsx'te birebir aynı çakışma vardı — aynı PR'da düzeltildi.)
99
114
  <span
100
- aria-hidden
115
+ ref={ref}
101
116
  className={cn("moonui-theme", "flex h-9 w-9 items-center justify-center", className)}
102
117
  {...props}
103
118
  >
104
- <MoreHorizontal className="h-4 w-4" />
119
+ <span aria-hidden="true" className="flex items-center">
120
+ <MoreHorizontal className="h-4 w-4" />
121
+ </span>
105
122
  <span className="sr-only">More pages</span>
106
123
  </span>
107
- )
124
+ ))
108
125
  PaginationEllipsis.displayName = "PaginationEllipsis"
109
126
 
110
127
  export {
@@ -115,4 +132,7 @@ export {
115
132
  PaginationLink,
116
133
  PaginationNext,
117
134
  PaginationPrevious,
118
- }
135
+ }
136
+
137
+ // #315: Props tiplerinin hiçbiri export edilmiyordu.
138
+ export type { PaginationLinkProps }
@@ -5,16 +5,36 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5
5
 
6
6
  import { cn } from "../../lib/utils"
7
7
 
8
+ export interface ScrollAreaProps
9
+ extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
10
+ /**
11
+ * #308-1: Kaydırma alanının klavyeyle odaklanabilir olup olmayacağı. Varsayılan `true`
12
+ * — odaklanabilir içeriği olmayan bir kaydırma alanı aksi hâlde klavyeyle erişilemez
13
+ * (WCAG 2.1.1). Fazladan sekme durağı istenmeyen durumlarda `false`.
14
+ */
15
+ focusable?: boolean
16
+ }
17
+
8
18
  const ScrollArea = React.forwardRef<
9
19
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
11
- >(({ className, children, ...props }, ref) => (
20
+ ScrollAreaProps
21
+ >(({ className, children, focusable = true, ...props }, ref) => (
12
22
  <ScrollAreaPrimitive.Root
13
23
  ref={ref}
14
24
  className={cn("relative overflow-hidden", className)}
15
25
  {...props}
16
26
  >
17
- <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
27
+ {/*
28
+ #308-1: Viewport'a `tabIndex` VERİLMİYORDU ve bu davranış Radix'ten kalıtsal DEĞİL
29
+ (`@radix-ui/react-scroll-area` kaynağında tabIndex geçmiyor). Sonuç: içinde
30
+ odaklanabilir eleman bulunmayan bir ScrollArea klavyeyle HİÇ kaydırılamıyordu
31
+ (WCAG 2.1.1). Kaydırılabilir bölgenin odaklanabilir olması kanonik çözümdür;
32
+ ek sekme durağı istemeyen tüketici `focusable={false}` diyebilir.
33
+ */}
34
+ <ScrollAreaPrimitive.Viewport
35
+ tabIndex={focusable ? 0 : undefined}
36
+ className="h-full w-full rounded-[inherit]"
37
+ >
18
38
  {children}
19
39
  </ScrollAreaPrimitive.Viewport>
20
40
  <ScrollBar />
@@ -144,10 +144,25 @@ export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
144
144
  }
145
145
 
146
146
  const SkeletonElement = useMotion ? motion.div : "div";
147
-
147
+
148
148
  return (
149
149
  <SkeletonElement
150
150
  ref={ref}
151
+ /**
152
+ * #320: Skeleton hiçbir a11y işareti taşımıyordu — ne `aria-hidden`, ne `role`,
153
+ * ne `aria-busy`. Radix kullanmadığı için kalıtsal semantik de yok.
154
+ *
155
+ * Konvansiyon olarak `aria-hidden` seçildi (`role="status"` DEĞİL): tek bir yükleme
156
+ * durumu tipik olarak ONLARCA skeleton render eder (`SkeletonText` tek başına N satır
157
+ * basar) — her biri canlı bölge olsaydı ekran okuyucu aynı olayı N kez duyururdu.
158
+ * "Yükleniyor" duyurusunun sahibi `spinner.tsx:79-105` (`role="status"` +
159
+ * `aria-live="polite"` + `sr-only` etiket); skeleton onun görsel yer tutucusudur.
160
+ * Tüketici kapsayıcıya `aria-busy` koyarak durumu bildirir.
161
+ *
162
+ * Yalnız YER TUTUCU dalında: `isLoaded && children` dalı gerçek içerik döndürür ve
163
+ * yukarıda kasıtlı olarak işaretlenmemiştir.
164
+ */
165
+ aria-hidden="true"
151
166
  className={cn(
152
167
  "moonui-theme",
153
168
  skeletonVariants({ variant, size, shape, animation }),
@@ -9,22 +9,52 @@ import { cn } from "../../lib/utils";
9
9
  /* -------------------------------------------------------------------------------------------------
10
10
  * Tabs Root
11
11
  * -----------------------------------------------------------------------------------------------*/
12
+ type TabsOrientation = "horizontal" | "vertical";
13
+
14
+ /**
15
+ * #322: Yönelim için TEK KAYNAK.
16
+ *
17
+ * Eskiden üç ayrı yol vardı ve birbirini eziyordu:
18
+ * 1. Root `vertical` prop'undan `orientation` hesaplıyor, ARDINDAN `{...props}` yayıyordu
19
+ * → tüketicinin (ya da Radix'in kalıtsal) `orientation` prop'u `vertical`'ı SESSİZCE
20
+ * eziyordu.
21
+ * 2. `TabsList`/`TabsTrigger` kendi `orientation` prop'unu yalnız CVA sınıfına veriyor,
22
+ * Root'tan hiç beslenmiyordu → klavye yönü dikey, görünüm yatay kalabiliyordu.
23
+ *
24
+ * Artık Root çözülmüş yönelimi context ile dağıtıyor; alt component'ler kendi prop'unu
25
+ * verirse o kazanır (yerel ezme kasıtlı olarak korundu).
26
+ */
27
+ const TabsOrientationContext = React.createContext<TabsOrientation>("horizontal");
28
+
12
29
  interface TabsProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root> {
13
- /** Mobil görünümde dikey düzen için */
30
+ /**
31
+ * @deprecated `orientation="vertical"` kullanın — pro `MoonUITabsPro` ile aynı ad ve
32
+ * tip. `vertical` yalnız `orientation` verilmediğinde dikkate alınır.
33
+ */
14
34
  vertical?: boolean;
15
35
  }
16
36
 
17
37
  const Tabs = React.forwardRef<
18
38
  React.ElementRef<typeof TabsPrimitive.Root>,
19
39
  TabsProps
20
- >(({ vertical = false, ...props }, ref) => (
21
- <TabsPrimitive.Root
22
- ref={ref}
23
- className={cn("moonui-theme", props.className)}
24
- orientation={vertical ? "vertical" : "horizontal"}
25
- {...props}
26
- />
27
- ));
40
+ >(({ vertical = false, orientation, className, ...props }, ref) => {
41
+ const resolvedOrientation: TabsOrientation =
42
+ orientation ?? (vertical ? "vertical" : "horizontal");
43
+
44
+ return (
45
+ <TabsOrientationContext.Provider value={resolvedOrientation}>
46
+ <TabsPrimitive.Root
47
+ ref={ref}
48
+ // #322: `className` eskiden destructure EDİLMİYORDU; `cn("moonui-theme", …)`
49
+ // hesaplanıp ardından `{...props}` yayıldığı için tüketici `className` verdiğinde
50
+ // hesaplanan değer tümüyle eziliyor ve `moonui-theme` tema kapsamı düşüyordu.
51
+ className={cn("moonui-theme", className)}
52
+ orientation={resolvedOrientation}
53
+ {...props}
54
+ />
55
+ </TabsOrientationContext.Provider>
56
+ );
57
+ });
28
58
 
29
59
  Tabs.displayName = TabsPrimitive.Root.displayName;
30
60
 
@@ -77,10 +107,14 @@ const TabsList = React.forwardRef<
77
107
  TabsListProps
78
108
  >(({ className, variant, orientation, fullWidth, scrollable, ...props }, ref) => {
79
109
  // scrollable prop'unu DOM'a geçirmemek için destructure ediyoruz
110
+ // #322: yönelim Root'tan context ile geliyor; yerel prop verilirse o kazanır.
111
+ const contextOrientation = React.useContext(TabsOrientationContext);
112
+ const resolvedOrientation = orientation ?? contextOrientation;
113
+
80
114
  return (
81
115
  <TabsPrimitive.List
82
116
  ref={ref}
83
- className={cn("moonui-theme", tabsListVariants({ variant, orientation, fullWidth, scrollable, className }))}
117
+ className={cn("moonui-theme", tabsListVariants({ variant, orientation: resolvedOrientation, fullWidth, scrollable, className }))}
84
118
  {...props}
85
119
  />
86
120
  );
@@ -152,14 +186,19 @@ const TabsTrigger = React.forwardRef<
152
186
  fadeTabs = false,
153
187
  orientation,
154
188
  fullWidth,
155
- children,
156
- ...props
157
- }, ref) => (
189
+ children,
190
+ ...props
191
+ }, ref) => {
192
+ // #322: yönelim Root'tan context ile geliyor; yerel prop verilirse o kazanır.
193
+ const contextOrientation = React.useContext(TabsOrientationContext);
194
+ const resolvedOrientation = orientation ?? contextOrientation;
195
+
196
+ return (
158
197
  <TabsPrimitive.Trigger
159
198
  ref={ref}
160
199
  className={cn(
161
200
  "moonui-theme",
162
- tabsTriggerVariants({ variant, size, orientation, fullWidth }),
201
+ tabsTriggerVariants({ variant, size, orientation: resolvedOrientation, fullWidth }),
163
202
  fadeTabs && "data-[state=inactive]:opacity-60",
164
203
  className
165
204
  )}
@@ -176,7 +215,8 @@ const TabsTrigger = React.forwardRef<
176
215
  <span className="ml-2">{badge}</span>
177
216
  )}
178
217
  </TabsPrimitive.Trigger>
179
- ));
218
+ );
219
+ });
180
220
 
181
221
  TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
182
222
 
@@ -207,4 +247,14 @@ const TabsContent = React.forwardRef<
207
247
 
208
248
  TabsContent.displayName = TabsPrimitive.Content.displayName;
209
249
 
210
- export { Tabs, TabsList, TabsTrigger, TabsContent };
250
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants, tabsTriggerVariants };
251
+
252
+ // #322: Props interface'lerinin hiçbiri export edilmiyordu (pro tarafı `MoonUITabsProProps`
253
+ // ve `TabItem`'ı export ediyor — asimetriydi).
254
+ export type {
255
+ TabsProps,
256
+ TabsListProps,
257
+ TabsTriggerProps,
258
+ TabsContentProps,
259
+ TabsOrientation,
260
+ };