@moontra/moonui 6.24.0 → 6.26.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.
Files changed (56) hide show
  1. package/dist/cdn/moonui.css +2 -2
  2. package/dist/cdn/moonui.esm.js +12 -12
  3. package/dist/cdn/moonui.global.js +12 -12
  4. package/dist/index.d.mts +24 -6
  5. package/dist/index.d.ts +24 -6
  6. package/dist/index.js +436 -93
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +437 -95
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +1 -1
  11. package/src/__tests__/issue-544-kontrast.test.tsx +153 -145
  12. package/src/__tests__/issue-645-overlay-tab-focus.test.tsx +262 -0
  13. package/src/__tests__/issue-646-dark-scope-override.test.tsx +297 -0
  14. package/src/__tests__/issue-661-a11y.test.tsx +113 -0
  15. package/src/__tests__/issue-661-token-kontrast.test.tsx +82 -0
  16. package/src/components/ui/__tests__/accent-foreground-contrast.test.ts +197 -0
  17. package/src/components/ui/__tests__/accordion.test.tsx +6 -4
  18. package/src/components/ui/__tests__/alert.test.tsx +4 -1
  19. package/src/components/ui/__tests__/button.test.tsx +6 -1
  20. package/src/components/ui/__tests__/dark-token-contrast.test.ts +18 -18
  21. package/src/components/ui/__tests__/heading-level-policy.test.tsx +4 -4
  22. package/src/components/ui/__tests__/issue-643-card-token-parite.test.tsx +113 -0
  23. package/src/components/ui/__tests__/issue-649-button-aschild.test.tsx +199 -0
  24. package/src/components/ui/__tests__/issue-664-separator-dekoratif-rol.test.tsx +41 -0
  25. package/src/components/ui/__tests__/preset-color-shadowing.test.ts +10 -10
  26. package/src/components/ui/__tests__/primary-subtle-token.test.ts +7 -7
  27. package/src/components/ui/__tests__/progress-secondary-contrast.test.ts +38 -38
  28. package/src/components/ui/__tests__/progress-warning-error-contrast.test.ts +64 -64
  29. package/src/components/ui/__tests__/quality-group-de.test.tsx +2 -2
  30. package/src/components/ui/__tests__/responsive-color-invariance.test.ts +20 -20
  31. package/src/components/ui/__tests__/select.test.tsx +6 -1
  32. package/src/components/ui/__tests__/semantic-token-usage.test.ts +23 -23
  33. package/src/components/ui/__tests__/size-scale-convention.test.ts +10 -10
  34. package/src/components/ui/__tests__/slider-accent-contrast.test.ts +142 -136
  35. package/src/components/ui/__tests__/slider-secondary-contrast.test.ts +60 -60
  36. package/src/components/ui/__tests__/slider-warning-error-contrast.test.ts +100 -100
  37. package/src/components/ui/__tests__/switch-variant-contrast.test.tsx +103 -103
  38. package/src/components/ui/__tests__/tooltip.test.tsx +122 -0
  39. package/src/components/ui/accordion.tsx +16 -3
  40. package/src/components/ui/alert.tsx +1 -1
  41. package/src/components/ui/badge.tsx +12 -1
  42. package/src/components/ui/button.tsx +95 -24
  43. package/src/components/ui/card.tsx +59 -5
  44. package/src/components/ui/dropdown-menu.tsx +99 -5
  45. package/src/components/ui/file-upload.tsx +4 -0
  46. package/src/components/ui/index.ts +4 -1
  47. package/src/components/ui/popover-pro.tsx +41 -0
  48. package/src/components/ui/popover.tsx +162 -7
  49. package/src/components/ui/rating.tsx +24 -2
  50. package/src/components/ui/select.tsx +4 -4
  51. package/src/components/ui/simple-editor.tsx +3 -18
  52. package/src/components/ui/tabs.tsx +16 -1
  53. package/src/components/ui/textarea.tsx +6 -1
  54. package/src/components/ui/toast.tsx +17 -2
  55. package/src/components/ui/tooltip.tsx +154 -2
  56. package/src/styles/tokens.css +34 -2
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react";
4
+ import { Slot, Slottable } from "@radix-ui/react-slot";
4
5
  import { cva, type VariantProps } from "class-variance-authority";
5
6
  import { cn } from "../../lib/utils";
6
7
  import { Loader2 } from "lucide-react";
@@ -86,10 +87,15 @@ const buttonVariants = cva(
86
87
  "transition-all duration-200",
87
88
  ],
88
89
  success: [
90
+ // Koyu temada `--success` (#16a34a) foreground'uyla 3.15 veriyordu (AA 4.5
91
+ // ister) — `--destructive`in #544'teki birebir aynı kusuru. Açık tema
92
+ // dokunulmadı (#442 kuralı: geçen yüzeyde regresyon riski açma).
93
+ // Dolgu opak `success` skalasına bağlandı: -600 hâlâ 3.15, ölçümle geçen
94
+ // en açık kademe -700 (4.79).
89
95
  "bg-success text-success-foreground",
90
- "dark:bg-success dark:text-success-foreground",
91
- "hover:bg-success/90 dark:hover:bg-success/90",
92
- "active:bg-success/80 dark:active:bg-success/80",
96
+ "dark:bg-success-700 dark:text-success-foreground",
97
+ "hover:bg-success/90 dark:hover:bg-success-800",
98
+ "active:bg-success/80 dark:active:bg-success-900",
93
99
  "shadow-sm hover:shadow-md",
94
100
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
95
101
  "transition-all duration-200",
@@ -111,11 +117,16 @@ const buttonVariants = cva(
111
117
  "transition-all duration-300",
112
118
  ],
113
119
  glow: [
114
- "bg-blue-500 text-white",
115
- "shadow-[0_0_30px_rgba(59,130,246,0.5)]",
116
- "hover:shadow-[0_0_40px_rgba(59,130,246,0.7)]",
117
- "active:shadow-[0_0_20px_rgba(59,130,246,0.4)]",
118
- "hover:bg-blue-600",
120
+ // blue-500 (#3b82f6) beyaz metinle 3.68 veriyordu (AA 4.5 ister).
121
+ // Ölçümle geçen en açık kademe blue-600 (#2563eb → 5.17); parıltı
122
+ // gölgesi `--glow-blue-rgb` üzerinden AYNI kaldı, yalnız dolgu koyulaştı.
123
+ "bg-blue-600 text-white",
124
+ // #664 S3: rgba(59,130,246,…) → --glow-blue-rgb (tokens.css). Değer AYNI
125
+ // (Tailwind blue-500), yalnız merkezi token'a taşındı — görsel değişiklik yok.
126
+ "shadow-[0_0_30px_rgba(var(--glow-blue-rgb),0.5)]",
127
+ "hover:shadow-[0_0_40px_rgba(var(--glow-blue-rgb),0.7)]",
128
+ "active:shadow-[0_0_20px_rgba(var(--glow-blue-rgb),0.4)]",
129
+ "hover:bg-blue-700",
119
130
  "focus-visible:ring-blue-500/50",
120
131
  "transition-all duration-300",
121
132
  ],
@@ -124,7 +135,12 @@ const buttonVariants = cva(
124
135
  "hover:bg-primary/20",
125
136
  "active:bg-primary/30",
126
137
  "focus-visible:ring-primary/50",
127
- "dark:bg-primary/20 dark:text-primary-foreground",
138
+ // Koyu temada `bg-primary/20` + `text-primary-foreground` 1.76 veriyordu:
139
+ // dark `--primary` neredeyse beyaz olduğu için /20 açık-gri bir zemin
140
+ // üretiyor, `--primary-foreground` ise koyu — ikisi birbirini yiyordu.
141
+ // #354'ün sidebar'da kanıtlanmış çözümü: subtle token ÇİFTİ (zemin+metin
142
+ // birlikte tanımlı, koyu temada koyu zemin + açık metin).
143
+ "dark:bg-primary-subtle dark:text-primary-subtle-foreground",
128
144
  "dark:hover:bg-primary/30 dark:active:bg-primary/40",
129
145
  "transition-colors duration-200",
130
146
  ],
@@ -143,8 +159,9 @@ const buttonVariants = cva(
143
159
  "bg-transparent text-green-400",
144
160
  "border-2 border-green-400",
145
161
  "hover:bg-green-400 hover:text-gray-900",
146
- "hover:shadow-[0_0_30px_rgba(74,222,128,0.6)]",
147
- "active:shadow-[0_0_15px_rgba(74,222,128,0.4)]",
162
+ // #664 S3: rgba(74,222,128,…) → --glow-green-rgb (tokens.css), piksel-özdeş.
163
+ "hover:shadow-[0_0_30px_rgba(var(--glow-green-rgb),0.6)]",
164
+ "active:shadow-[0_0_15px_rgba(var(--glow-green-rgb),0.4)]",
148
165
  "focus-visible:ring-green-400/50",
149
166
  "transition-all duration-300",
150
167
  ],
@@ -200,14 +217,21 @@ export interface ButtonProps
200
217
  * @param props.rightIcon - Icon on the right side
201
218
  * @param props.fullWidth - Full width display
202
219
  * @param props.rounded - Corner rounding style (default, full, none)
203
- * @param props.asChild - For Radix UI polymorphic component
220
+ * @param props.asChild - Renders the single child element instead of a native
221
+ * `<button>` (Radix `Slot`), e.g. `<Button asChild><a href="/x">Go</a></Button>`.
222
+ * The `buttonVariants` classes, the loading spinner and `leftIcon`/`rightIcon`
223
+ * are merged INTO that child. Because non-button elements ignore the native
224
+ * `disabled` attribute, `disabled`/`loading` are expressed as `aria-disabled` +
225
+ * `data-disabled` plus inert styling, and activation is blocked in the click
226
+ * handler. Falls back to a native `<button>` when the child is not a valid
227
+ * React element.
204
228
  */
205
229
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
206
- ({
207
- className,
208
- variant,
209
- size,
210
- rounded,
230
+ ({
231
+ className,
232
+ variant,
233
+ size,
234
+ rounded,
211
235
  fullWidth,
212
236
  asChild = false,
213
237
  loading = false,
@@ -215,27 +239,74 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
215
239
  rightIcon,
216
240
  children,
217
241
  disabled,
218
- ...props
242
+ onClick,
243
+ ...props
219
244
  }, ref) => {
220
- // Radix UI'ın asChild pattern'i için - "as" prop yerine
221
- const Comp = asChild ? "button" : "button";
222
-
245
+ // #649: `asChild` daha önce `asChild ? "button" : "button"` idi — ternary'nin iki
246
+ // dalı BİREBİR aynıydı, yani prop tipte ve docs'ta vaat edilmesine rağmen hiçbir
247
+ // şey yapmıyordu. Radix `Slot`a bağlandı (kardeş desen: breadcrumb #309, card #310).
248
+ //
249
+ // Slot TEK ve GEÇERLİ bir element çocuğu ister. `asChild` bugüne dek etkisiz
250
+ // olduğu için `<Button asChild>düz metin</Button>` yazan mevcut kullanım sorunsuz
251
+ // çalışıyordu; doğrudan Slot'a düşseydi sessizce BOŞ render ederdi. Bu yüzden
252
+ // çocuk element değilse native <button>'a geri düşülür.
253
+ const renderAsSlot = asChild && React.isValidElement(children);
254
+ const Comp = renderAsSlot ? Slot : "button";
255
+ const isDisabled = disabled || loading;
256
+
257
+ // asChild modunda çocuk <a>/<Link>/<span> olabilir; bu elemanlar native `disabled`
258
+ // özniteliğini TANIMAZ:
259
+ // • React onu geçersiz bir öznitelik olarak DOM'a basar,
260
+ // • `:disabled` hiç eşleşmez → cva'daki `disabled:*` stilleri ölü kalır,
261
+ // • yardımcı teknoloji "devre dışı" duyurusunu alamaz.
262
+ // Bu yüzden devre dışılık Slot dalında ARIA + veri özniteliği ile ifade edilir;
263
+ // native `disabled` yalnız gerçek <button> dalında verilir.
264
+ const disabledProps = renderAsSlot
265
+ ? {
266
+ "aria-disabled": isDisabled || undefined,
267
+ "data-disabled": isDisabled ? "" : undefined,
268
+ }
269
+ : { disabled: isDisabled };
270
+
271
+ // asChild + devre dışı: `pointer-events-none` fareyi keser ama bir <a> klavyeden
272
+ // (Enter) hâlâ etkinleşir. Etkinleştirme burada durdurulur; `preventDefault`
273
+ // bağlantı gezinmesini de iptal eder.
274
+ // SINIR: Radix `Slot` olay işleyicilerini birleştirirken ÖNCE çocuğun kendi
275
+ // işleyicisini çağırır — çocuğa doğrudan bağlanmış bir `onClick` engellenemez.
276
+ const handleClick =
277
+ renderAsSlot && isDisabled
278
+ ? (event: React.MouseEvent<HTMLButtonElement>) => {
279
+ event.preventDefault();
280
+ event.stopPropagation();
281
+ }
282
+ : onClick;
283
+
223
284
  return (
224
285
  <Comp
225
- className={cn("moonui-theme", buttonVariants({ variant, size, rounded, fullWidth, className }))}
286
+ className={cn(
287
+ "moonui-theme",
288
+ buttonVariants({ variant, size, rounded, fullWidth, className }),
289
+ // `:disabled` eşleşmediği için görsel/etkileşimsel devre dışılık Slot
290
+ // dalında açık sınıflarla verilir.
291
+ renderAsSlot && isDisabled && "opacity-50 cursor-not-allowed pointer-events-none"
292
+ )}
226
293
  ref={ref}
227
- disabled={disabled || loading}
294
+ {...disabledProps}
228
295
  // Görsel spinner + data-loading vardı ama yardımcı teknolojiye meşgul
229
296
  // durumu hiç duyurulmuyordu.
230
297
  aria-busy={loading || undefined}
231
298
  data-loading={loading ? "" : undefined}
232
299
  {...props}
300
+ onClick={handleClick}
233
301
  >
234
302
  {loading && (
235
303
  <Loader2 className="w-4 h-4 animate-spin" aria-hidden="true" />
236
304
  )}
237
305
  {!loading && leftIcon}
238
- {children}
306
+ {/* #649: Slot dalında spinner ve ikonlar `Slottable` sayesinde çocuğun İÇİNE
307
+ taşınır; aksi hâlde Slot birden çok çocuk görüp `Children.only` ile patlardı
308
+ ve `loading`/ikon sözleşmesi asChild'da tamamen kaybolurdu. */}
309
+ {renderAsSlot ? <Slottable>{children}</Slottable> : children}
239
310
  {!loading && rightIcon}
240
311
  </Comp>
241
312
  );
@@ -21,17 +21,71 @@ import { microInteractionVariants, hoverAnimations, tapAnimations } from "@/lib/
21
21
  // • `asChild`: yalnız tipteydi, gövdede hiç okunmuyordu → `...props` ile native div'e
22
22
  // sızıyor (`<div aschild="true">`, React uyarısı). Radix `Slot`a bağlandı.
23
23
 
24
+ // #646: cva TABANINDAKİ `dark:bg-gray-900` KALDIRILDI — `bg-card` token'ı zaten
25
+ // aynı dizede duruyordu, ikisi birden gereksiz bir İKİNCİ katmandı.
26
+ //
27
+ // NEDEN kaldırıldı (mekanizma): `tailwind-merge` yalnız AYNI değiştirici (modifier)
28
+ // kümesindeki sınıfları çakıştırır. Tüketicinin verdiği ÖNEKSİZ `bg-white/90`,
29
+ // `dark:` önekli `dark:bg-gray-900` ile aynı grupta görülmediği için ikisi birden
30
+ // hayatta kalıyor; koyu temada `.dark .dark\:bg-gray-900` daha yüksek özgüllükle
31
+ // kazanıyordu. Yani component, tüketicinin arka planını SESSİZCE eziyordu.
32
+ // Ölçüm (gradient-flow demosu, koyu tema): kart rgb(17,24,39)'de kalıyor, üstündeki
33
+ // `text-gray-900` metinler TAM 1.00:1 — matematiksel olarak görünmez. Kaldırıldıktan
34
+ // sonra `bg-white/90` kazanıyor: 1.00 → 14.46, 2.35 → 6.16, 1.72 → 8.40.
35
+ //
36
+ // GÖRSEL DELTA (tüketici override'ı YOKken): koyu temada varsayılan kart
37
+ // rgb(17,24,39) → rgb(3,7,17) (= `bg-card`). Kartın kendi metniyle kontrastı
38
+ // 16.12 → 18.31'e ÇIKAR; kenarlık (`dark:border-gray-800`) yüzeyden 1.17 → 1.35
39
+ // ile daha iyi ayrışır. Bu, `elevated` varyantının #310'dan beri zaten yayımladığı
40
+ // koyu yüzeyin ta kendisidir (`dark:bg-card`) — yani yeni bir görünüm değil.
41
+ // Açık temada hiçbir değişiklik yok (`dark:` sınıfı orada zaten uygulanmıyordu).
42
+ //
43
+ // #643 (TERS PARİTE — free geride): #646 sonrasında bu dosyada HÂLÂ 12 ham `gray-*`
44
+ // sınıfı duruyordu (issue "6" diyordu; yorumsuz gerçek sayım 12: 3+2+3+2+2), pro
45
+ // `card.tsx`te 0. Tümü token'a taşındı. Ölçümler (WCAG 2.1 kontrast oranı, önce→sonra):
46
+ //
47
+ // • `dark:text-gray-100` / `dark:text-gray-200` (4 yer) → KALDIRILDI.
48
+ // `text-card-foreground` zaten aynı dizede; `dark:` önekli sınıf onu ezmekle
49
+ // kalmıyor, #646'nın `dark:bg-gray-900` için belgelediği tuzağın aynısıyla
50
+ // TÜKETİCİNİN `text-*` override'ını da sessizce eziyordu (farklı değiştirici
51
+ // kümesi → `tailwind-merge` çakıştırmıyor, `.dark` özgüllüğü kazanıyor).
52
+ // Metin/yüzey kontrastı koyu temada 18.31 → 16.19 (AA 4.5'in 3.6 katı).
53
+ // • `dark:border-gray-800` → tabanda `border-border`. Koyu tema 1.37 → 1.36.
54
+ // BONUS: taban yalnız `border` (genişlik) taşıdığı için AÇIK temada kenar rengi
55
+ // token'dan değil Tailwind varsayılanı `gray-200`den geliyordu; artık token.
56
+ // Açık tema 1.24 → 1.23 (görsel olarak ayırt edilemez).
57
+ // • `outline` › `dark:border-gray-700` → KALDIRILDI (taban `border-border` yeterli).
58
+ // Koyu tema kenar/zemin 1.95 → 1.36. Bu bilinçli: `gray-700` sistemdeki TEK
59
+ // aykırı değerdi — input/popover/dialog dahil her component koyu temada zaten
60
+ // `--border`i (1.36) kullanıyor. tokens.css:163-166 `--border`i açıkça
61
+ // "dekoratif" ilan eder (metin eşiği uygulanmaz). Kart, sistemle hizalandı.
62
+ // • `filled` › `dark:bg-gray-800` → `dark:bg-muted`. Yüzey/sayfa-zemini AYRIŞMASI
63
+ // 1.37 → 1.49 (İYİLEŞTİ); metin/yüzey 13.34 → 10.87 (AA'nın 2.4 katı).
64
+ // #368 `--muted`i tam bu rol için ("DEKORATİF YÜZEY", koyu temada ayrışmalı)
65
+ // ayarlamıştı.
66
+ // ⚠ Pro'nun seçimi (`dark:bg-secondary`) BİLİNÇLİ OLARAK REDDEDİLDİ: koyu temada
67
+ // `--secondary` = rgb(2,8,23), sayfa zemini rgb(3,7,17) → ayrışma 1.01, yani
68
+ // `filled` yüzeyi fiilen GÖRÜNMEZ. Ham-gri ihlalini görünmez-yüzey ihlaliyle
69
+ // takas etmemek için (#545 dersi) pro yerine `--muted` alındı; pro `card.tsx`
70
+ // de aynı ölçümle `--muted`e çekildi.
71
+ // • `dark:shadow-gray-900/20`, `…/30`, `dark:shadow-gray-950/10` → aynı alfalarla
72
+ // `dark:shadow-black`. Gölge rengi; koyu zeminde ölçülebilir kontrast rolü yok,
73
+ // pro idiyomu. (Not: bu blokta bilerek ardışık eğik-yıldız yazılmaz — depodaki
74
+ // regex tabanlı tarayıcılar onu blok yorum başlangıcı sanıp gerisini yutuyor.)
75
+ // • `glass` › `dark:border-gray-700/30` → `dark:border-white/10` (kenar/cam yüzey
76
+ // 1.15 → 1.23, İYİLEŞTİ), `dark:bg-gray-800/10` → `dark:bg-black/10`
77
+ // (yüzey/zemin 1.02 → 1.01; cam varyantı zaten arkasındaki içeriği gösterir).
24
78
  const cardVariants = cva(
25
- "rounded-lg border bg-card text-card-foreground shadow-sm dark:shadow-gray-900/20 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 transition-all duration-200",
79
+ "rounded-lg border border-border bg-card text-card-foreground shadow-sm dark:shadow-black/20 transition-all duration-200",
26
80
  {
27
81
  variants: {
28
82
  variant: {
29
83
  default: "",
30
- outline: "border border-border shadow-none bg-transparent dark:border-gray-700 dark:bg-transparent dark:text-gray-200",
31
- filled: "border-none bg-secondary dark:bg-gray-800 dark:text-gray-100 dark:shadow-inner dark:shadow-gray-950/10",
32
- elevated: "border-none shadow-md dark:shadow-lg dark:shadow-gray-900/30 dark:bg-card dark:text-gray-100",
84
+ outline: "border border-border shadow-none bg-transparent dark:bg-transparent",
85
+ filled: "border-none bg-secondary dark:bg-muted dark:shadow-inner dark:shadow-black/10",
86
+ elevated: "border-none shadow-md dark:shadow-lg dark:shadow-black/30 dark:bg-card",
33
87
  gradient: "border-none bg-gradient-to-br from-primary/5 to-primary/10 dark:from-primary/10 dark:to-primary/20",
34
- glass: "border border-white/20 bg-white/10 backdrop-blur-md dark:border-gray-700/30 dark:bg-gray-800/10",
88
+ glass: "border border-white/20 bg-white/10 backdrop-blur-md dark:border-white/10 dark:bg-black/10",
35
89
  },
36
90
  size: {
37
91
  md: "",
@@ -6,7 +6,91 @@ import { Check, ChevronRight, Circle } from "lucide-react"
6
6
 
7
7
  import { cn } from "../../lib/utils"
8
8
 
9
- const DropdownMenu = DropdownMenuPrimitive.Root
9
+ /* #645 Menü açıkken Tab hiçbir şey yapmıyordu.
10
+ *
11
+ * KÖK NEDEN (Radix kaynağı, `@radix-ui/react-menu` → `MenuContentImpl`):
12
+ * if (isKeyDownInside) { if (event.key === "Tab") event.preventDefault(); ... }
13
+ * Radix, Tab'ı bilinçli olarak yutuyor ("menus should not be navigated using tab
14
+ * key") ama karşılığında HİÇBİR ŞEY yapmıyor — menü ne kapanıyor ne odak
15
+ * ilerliyor. Üstelik `modal` menüde `FocusScope` da `trapped` olduğu için odak
16
+ * konteynerden çıkamıyor. Yani kusur sarmalayıcımızda değil, devraldığımız
17
+ * Radix davranışında.
18
+ *
19
+ * SEÇİLEN SEMANTİK: WAI-ARIA APG "menu button" — menü TEK bir tab durağıdır;
20
+ * Tab menüyü kapatır ve odağı tetikleyiciye döndürür (Escape ile aynı çıkış
21
+ * yolu, dolayısıyla ailede tutarlı). Alt menüde basılan Tab da kökü kapatır.
22
+ *
23
+ * Kapatma için köke kontrollü bir açık/kapalı durumu takılır; `close` yalnız bu
24
+ * dosyanın içinde, public export açılmadan paylaşılır (#667).
25
+ */
26
+ const DropdownMenuCloseContext = React.createContext<(() => void) | null>(null)
27
+
28
+ type DropdownMenuRootProps = React.ComponentPropsWithoutRef<
29
+ typeof DropdownMenuPrimitive.Root
30
+ >
31
+
32
+ const DropdownMenu = ({
33
+ open,
34
+ defaultOpen,
35
+ onOpenChange,
36
+ children,
37
+ ...props
38
+ }: DropdownMenuRootProps) => {
39
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
40
+ defaultOpen ?? false
41
+ )
42
+ const isControlled = open !== undefined
43
+ const currentOpen = isControlled ? open : uncontrolledOpen
44
+
45
+ const handleOpenChange = React.useCallback(
46
+ (nextOpen: boolean) => {
47
+ if (!isControlled) setUncontrolledOpen(nextOpen)
48
+ onOpenChange?.(nextOpen)
49
+ },
50
+ [isControlled, onOpenChange]
51
+ )
52
+
53
+ const close = React.useCallback(() => handleOpenChange(false), [
54
+ handleOpenChange,
55
+ ])
56
+
57
+ return (
58
+ <DropdownMenuCloseContext.Provider value={close}>
59
+ <DropdownMenuPrimitive.Root
60
+ {...props}
61
+ open={currentOpen}
62
+ onOpenChange={handleOpenChange}
63
+ >
64
+ {children}
65
+ </DropdownMenuPrimitive.Root>
66
+ </DropdownMenuCloseContext.Provider>
67
+ )
68
+ }
69
+ DropdownMenu.displayName = "DropdownMenu"
70
+
71
+ /**
72
+ * Menü içeriğinde Tab'ı WAI-ARIA menü sözleşmesine bağlayan ortak işleyici.
73
+ * Alt menülerden köpüren tuş olayları yok sayılır — onları kendi içerikleri ele alır.
74
+ */
75
+ function useDropdownMenuTabDismiss(
76
+ onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>
77
+ ) {
78
+ const close = React.useContext(DropdownMenuCloseContext)
79
+ return (event: React.KeyboardEvent<HTMLDivElement>) => {
80
+ onKeyDown?.(event)
81
+ if (event.defaultPrevented) return
82
+ if (event.key !== "Tab") return
83
+ if (event.altKey || event.ctrlKey || event.metaKey) return
84
+ if (!close) return
85
+ const target = event.target as HTMLElement | null
86
+ if (target?.closest("[data-radix-menu-content]") !== event.currentTarget) {
87
+ return
88
+ }
89
+ // Odağı Radix'in `onCloseAutoFocus`'u tetikleyiciye geri taşır.
90
+ event.preventDefault()
91
+ close()
92
+ }
93
+ }
10
94
 
11
95
  const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
96
 
@@ -43,27 +127,36 @@ DropdownMenuSubTrigger.displayName =
43
127
  const DropdownMenuSubContent = React.forwardRef<
44
128
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
45
129
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
46
- >(({ className, ...props }, ref) => (
130
+ >(({ className, onKeyDown, ...props }, ref) => {
131
+ // #645: alt menüde Tab da kökü kapatır (WAI-ARIA: tüm ata menüler kapanır).
132
+ const handleKeyDown = useDropdownMenuTabDismiss(onKeyDown)
133
+ return (
47
134
  <DropdownMenuPrimitive.SubContent
48
135
  ref={ref}
136
+ onKeyDown={handleKeyDown}
49
137
  className={cn(
50
138
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
139
  className
52
140
  )}
53
141
  {...props}
54
142
  />
55
- ))
143
+ )
144
+ })
56
145
  DropdownMenuSubContent.displayName =
57
146
  DropdownMenuPrimitive.SubContent.displayName
58
147
 
59
148
  const DropdownMenuContent = React.forwardRef<
60
149
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61
150
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62
- >(({ className, sideOffset = 4, ...props }, ref) => (
151
+ >(({ className, sideOffset = 4, onKeyDown, ...props }, ref) => {
152
+ // #645: Tab menüyü kapatır, odak tetikleyiciye döner.
153
+ const handleKeyDown = useDropdownMenuTabDismiss(onKeyDown)
154
+ return (
63
155
  <DropdownMenuPrimitive.Portal>
64
156
  <DropdownMenuPrimitive.Content
65
157
  ref={ref}
66
158
  sideOffset={sideOffset}
159
+ onKeyDown={handleKeyDown}
67
160
  className={cn(
68
161
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
69
162
  className
@@ -71,7 +164,8 @@ const DropdownMenuContent = React.forwardRef<
71
164
  {...props}
72
165
  />
73
166
  </DropdownMenuPrimitive.Portal>
74
- ))
167
+ )
168
+ })
75
169
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76
170
 
77
171
  const DropdownMenuItem = React.forwardRef<
@@ -376,6 +376,10 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
376
376
  <motion.div
377
377
  className={cn(
378
378
  "relative border-2 border-dashed rounded-lg p-8 text-center transition-colors",
379
+ // #661-2: dosya girdisi `opacity-0` ile tum alani kapliyor; klavyeyle
380
+ // Tab'landiginda HICBIR gorsel iz yoktu (WCAG 2.4.7). Odak halkasi
381
+ // sarmalayiciya `focus-within` ile bagli — kutunun kendisi odaklanamaz.
382
+ "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
379
383
  isDragOver && "border-primary bg-primary/5",
380
384
  disabled && "opacity-50 cursor-not-allowed",
381
385
  !disabled && "hover:border-primary/50 cursor-pointer"
@@ -363,7 +363,10 @@ export type {
363
363
  } from "./select";
364
364
 
365
365
  // Separator
366
- export { Separator as MoonUISeparator } from "./separator";
366
+ // #643 (#667 ile açıldı) TERS PARİTE kapatıldı: pro barrel `MoonUISeparatorProProps`i
367
+ // dışa açıyordu, free tarafta MoonUI önekli yüzey eksikti.
368
+ export { Separator as MoonUISeparator, separatorVariants as moonUISeparatorVariants } from "./separator";
369
+ export type { SeparatorProps as MoonUISeparatorProps } from "./separator";
367
370
 
368
371
  // SimpleEditor
369
372
  export { SimpleEditor as MoonUISimpleEditor } from "./simple-editor";
@@ -1,5 +1,46 @@
1
1
  "use client"
2
2
 
3
+ /**
4
+ * #664 (S9) — ÖLÜ DOSYA. Silinmedi; gerekçesi aşağıda. Silmeden ÖNCE bu notu okuyun.
5
+ *
6
+ * ── DURUM: bu dosya hiçbir yerden erişilemiyor ───────────────────────────────
7
+ * Dört kanıt (2026-08-28 ölçümü):
8
+ * 1. Barrel'da yok — `components/ui/index.ts`, `src/index.tsx` ve
9
+ * `popover.tsx` içinde `popover-pro` grep'i **0**.
10
+ * 2. Yayımlı yüzeyde yok — `packages/moonui/dist/index.d.ts` içinde
11
+ * `PopoverHeaderPro` / `PopoverContentProProps` **0**.
12
+ * 3. Tüketici yok — repo genelinde bu dosyayı import eden **0** modül.
13
+ * 4. Kanonik başka yerde — docs'un GERÇEKTEN kullandığı Pro popover
14
+ * `MoonUIPopoverPro`'dur ve
15
+ * `packages/moonui-pro/src/components/ui/popover.tsx`te
16
+ * yaşar (`src/docs-config/popover.config.tsx:47-51`
17
+ * oradan import eder; yayımlı moonui-pro dist'inde var).
18
+ *
19
+ * ── NEDEN EXPORT EDİLMİYOR (kalıcı hüküm) ────────────────────────────────────
20
+ * Export etmek kusuru P3'ten P2'ye YÜKSELTİR: `customBackdrop` (:~215) ve
21
+ * `focusTrap` (:~219) ilan edilip varsayılanlarıyla destructure ediliyor ama
22
+ * gövdede HİÇ okunmuyor — public API'ye iki ölü prop eklemek olurdu.
23
+ * Ayrıca #667 registry kilidi: yayımlanmamış bir export eklenirse registry
24
+ * *tazelik* ya da *tutarlılık* kapısından biri mutlaka kırmızıya döner.
25
+ *
26
+ * ── NEDEN BU KOŞUDA SİLİNMEDİ (gerekçe — "zahmetli" DEĞİL) ───────────────────
27
+ * `scripts/lib/free-registry.js:285-288` free registry'yi diskteki dosya
28
+ * listesinden üretir ve barrel'da olmayan her modülü `excluded.notExported`
29
+ * altına yazar. Bu dosya şu an `free-registry.generated.json:27-29`ta öyle
30
+ * duruyor. Dosyayı silmek üretilen JSON'u ZORUNLU olarak değiştirir →
31
+ * `npm run check:free-registry` kırmızıya döner ve düzeltmenin tek yolu
32
+ * registry çıktısını yeniden üretmektir. Bu koşunun kısıtı registry generated
33
+ * dosyalarını yeniden üretmeyi yasaklıyor.
34
+ *
35
+ * ── YAPILACAK: #667 yayın kontrol listesinde sil ─────────────────────────────
36
+ * #667 zaten bump anında `build:free-registry` koşup çıktıyı commit'lemeyi
37
+ * planlıyor. Silme O ADIMLA BİRLİKTE yapılmalı — tek komut:
38
+ * git rm packages/moonui/src/components/ui/popover-pro.tsx
39
+ * npm run build:free-registry
40
+ * Silme kararı teknik olarak ONAYLI; bekleyen tek şey registry'yi yeniden
41
+ * üretme iznidir.
42
+ */
43
+
3
44
  import * as React from "react"
4
45
  import * as PopoverPrimitive from "@radix-ui/react-popover"
5
46
  import { motion, AnimatePresence, useSpring, useTransform } from "framer-motion"