@moontra/moonui 6.23.0 → 6.25.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 (57) 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 +448 -93
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +449 -95
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +1 -1
  11. package/src/__tests__/issue-544-kontrast.test.tsx +609 -0
  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__/badge.test.tsx +5 -1
  20. package/src/components/ui/__tests__/button.test.tsx +6 -1
  21. package/src/components/ui/__tests__/dark-token-contrast.test.ts +18 -18
  22. package/src/components/ui/__tests__/heading-level-policy.test.tsx +4 -4
  23. package/src/components/ui/__tests__/issue-643-card-token-parite.test.tsx +113 -0
  24. package/src/components/ui/__tests__/issue-649-button-aschild.test.tsx +199 -0
  25. package/src/components/ui/__tests__/issue-664-separator-dekoratif-rol.test.tsx +41 -0
  26. package/src/components/ui/__tests__/preset-color-shadowing.test.ts +10 -10
  27. package/src/components/ui/__tests__/primary-subtle-token.test.ts +7 -7
  28. package/src/components/ui/__tests__/progress-secondary-contrast.test.ts +38 -38
  29. package/src/components/ui/__tests__/progress-warning-error-contrast.test.ts +64 -64
  30. package/src/components/ui/__tests__/quality-group-de.test.tsx +2 -2
  31. package/src/components/ui/__tests__/responsive-color-invariance.test.ts +20 -20
  32. package/src/components/ui/__tests__/select.test.tsx +6 -1
  33. package/src/components/ui/__tests__/semantic-token-usage.test.ts +23 -23
  34. package/src/components/ui/__tests__/size-scale-convention.test.ts +10 -10
  35. package/src/components/ui/__tests__/slider-accent-contrast.test.ts +142 -136
  36. package/src/components/ui/__tests__/slider-secondary-contrast.test.ts +60 -60
  37. package/src/components/ui/__tests__/slider-warning-error-contrast.test.ts +100 -100
  38. package/src/components/ui/__tests__/switch-variant-contrast.test.tsx +103 -103
  39. package/src/components/ui/__tests__/tooltip.test.tsx +122 -0
  40. package/src/components/ui/accordion.tsx +16 -3
  41. package/src/components/ui/alert.tsx +1 -1
  42. package/src/components/ui/badge.tsx +31 -4
  43. package/src/components/ui/button.tsx +88 -21
  44. package/src/components/ui/card.tsx +59 -5
  45. package/src/components/ui/dropdown-menu.tsx +99 -5
  46. package/src/components/ui/file-upload.tsx +4 -0
  47. package/src/components/ui/index.ts +4 -1
  48. package/src/components/ui/popover-pro.tsx +41 -0
  49. package/src/components/ui/popover.tsx +162 -7
  50. package/src/components/ui/rating.tsx +24 -2
  51. package/src/components/ui/select.tsx +4 -4
  52. package/src/components/ui/simple-editor.tsx +3 -18
  53. package/src/components/ui/tabs.tsx +16 -1
  54. package/src/components/ui/textarea.tsx +6 -1
  55. package/src/components/ui/toast.tsx +17 -2
  56. package/src/components/ui/tooltip.tsx +154 -2
  57. package/src/styles/tokens.css +34 -2
@@ -25,11 +25,22 @@ const badgeVariants = cva(
25
25
  // aynı varyant pro pakette token'lıydı → free ve pro `primary` için
26
26
  // farklı renk üretiyordu. Artık token'a bağlı (tema/karanlık moda duyarlı).
27
27
  // `primary` VARSAYILAN varyant olduğu için görünür bir renk değişimidir.
28
+ // #646: `dark:bg-primary/90` KALDIRILDI. `primary` VARSAYILAN varyant olduğu
29
+ // için varyant verilmeyen HER `<Badge>` bu sınıfı alıyordu; `tailwind-merge`
30
+ // `dark:` önekli arka planı tüketicinin ÖNEKSİZ `bg-*` sınıfıyla aynı grupta
31
+ // görmediğinden ikisi birden hayatta kalıyor ve koyu temada `dark:` özgüllükle
32
+ // kazanıyordu. Sonuç: `<Badge className="bg-green-500/20 text-green-300">`
33
+ // koyu temada near-white zemin + açık yeşil metin = 1.10:1 (ölçüldü,
34
+ // magnetic-button "Pending"; swipe-actions'ta 6 rozet 1.10–1.48 aralığında).
35
+ // ÖNEKSİZ `bg-primary` bırakıldı: tüketici sınıfıyla AYNI grupta çakışır ve
36
+ // doğru şekilde ezilir. Rozetin kendi görünümü BOZULMAZ — koyu temada
37
+ // `bg-primary` + `text-primary-foreground` 16.16 → 19.80'e çıkar
38
+ // (yarı saydamlık kalktığı için); açık temada değişiklik yok.
28
39
  primary: [
29
40
  "border-transparent bg-primary text-primary-foreground",
30
41
  "hover:bg-primary/90",
31
42
  "focus-visible:ring-primary/30 dark:focus-visible:ring-primary/40",
32
- "dark:bg-primary/90 dark:shadow-inner dark:shadow-primary/10",
43
+ "dark:shadow-inner dark:shadow-primary/10",
33
44
  ],
34
45
  secondary: [
35
46
  "border-transparent bg-secondary text-secondary-foreground",
@@ -44,11 +55,27 @@ const badgeVariants = cva(
44
55
  "focus-visible:ring-gray-400/30 dark:focus-visible:ring-gray-500/40",
45
56
  "dark:text-gray-100 dark:bg-transparent dark:backdrop-blur-sm",
46
57
  ],
58
+ // #544: `bg-error` (= --error, #ef4343) beyaz metinle 3.78 veriyordu (AA 4.5 ister),
59
+ // `dark:bg-error/90` ise gerçek koyu kart (#111827) üstünde 4.43 ile kalıyordu.
60
+ //
61
+ // TOKEN DEĞİŞTİRİLMEDİ: dark `--destructive`/`--error` METİN rolü için bilerek
62
+ // açık tutuluyor (tokens.css:518-526); değeri koyulaştırmak `text-destructive`
63
+ // yazan yüzleri kırardı ve dark-token-contrast.test.ts'in belgelediği uzlaşmayı
64
+ // bozardı. Bunun yerine #424/#442/#454'ün presedansı uygulandı: token'a değil
65
+ // VARYANTA dokunulup skalanın bir kademesine inildi.
66
+ //
67
+ // `-600` ölçümle seçildi — AA'yı geçen EN AÇIK kademe (beyazla 4.83; -500 3.76 ✗)
68
+ // ve bugünkü kırmızıya EN YAKIN aday (ΔE*ab 10.3; -700 → 16.2, `--destructive`
69
+ // light → 18.8). Hue 0° korunur, "uyarı kırmızısı" kimliği kaybolmaz.
70
+ //
71
+ // Skala kademeleri tema-BAĞIMSIZ (`.dark` onları ezmez) → tek sınıf iki temayı
72
+ // birden kapatır ve eski YARI SAYDAM `dark:bg-error/90`ın zemine bağımlılığı
73
+ // ortadan kalkar. Hover koyulaşır (-700, 6.47), açılmaz.
47
74
  destructive: [
48
- "border-transparent bg-error text-white",
49
- "hover:bg-error/90 dark:hover:bg-error/80",
75
+ "border-transparent bg-error-600 text-white",
76
+ "hover:bg-error-700 dark:hover:bg-error-700",
50
77
  "focus-visible:ring-error/30 dark:focus-visible:ring-error/25",
51
- "dark:bg-error/90 dark:text-white dark:shadow-inner dark:shadow-error/10",
78
+ "dark:text-white dark:shadow-inner dark:shadow-error/10",
52
79
  ],
53
80
  success: [
54
81
  "border-transparent bg-success text-success-foreground",
@@ -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";
@@ -67,11 +68,20 @@ const buttonVariants = cva(
67
68
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
68
69
  "transition-all duration-200",
69
70
  ],
71
+ // #544: AÇIK tema zaten geçiyordu (`--destructive` light = #e11313, foreground
72
+ // #f8fafc ile 4.66) — #442'nin kuralı gereği DOKUNULMADI, regresyon yüzeyi sıfır.
73
+ // Kırık olan yalnız KOYU temaydı: dark `--destructive` = #ef4343 metin rolü için
74
+ // bilerek açık bırakılmış bir değer (tokens.css:518-526) ve dolgu rolünde
75
+ // foreground'la 3.61 veriyor. Token'a dokunmadan dark dolgusu opak `error`
76
+ // skalasına bağlandı (-600 → 4.62; ölçümle seçilen en açık geçen kademe).
77
+ // Not: `dark:bg-destructive` KALDIRILDI, yanına eklenmedi — iki `dark:bg-*`
78
+ // utility'si aynı anda yazılırsa hangisinin kazanacağını sınıf dizgesi sırası
79
+ // DEĞİL Tailwind'in kendi iç sıralaması belirler.
70
80
  destructive: [
71
81
  "bg-destructive text-destructive-foreground",
72
- "dark:bg-destructive dark:text-destructive-foreground",
73
- "hover:bg-destructive/90 dark:hover:bg-destructive/90",
74
- "active:bg-destructive/80 dark:active:bg-destructive/80",
82
+ "dark:bg-error-600 dark:text-destructive-foreground",
83
+ "hover:bg-destructive/90 dark:hover:bg-error-700",
84
+ "active:bg-destructive/80 dark:active:bg-error-800",
75
85
  "shadow-sm hover:shadow-md",
76
86
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
77
87
  "transition-all duration-200",
@@ -103,9 +113,11 @@ const buttonVariants = cva(
103
113
  ],
104
114
  glow: [
105
115
  "bg-blue-500 text-white",
106
- "shadow-[0_0_30px_rgba(59,130,246,0.5)]",
107
- "hover:shadow-[0_0_40px_rgba(59,130,246,0.7)]",
108
- "active:shadow-[0_0_20px_rgba(59,130,246,0.4)]",
116
+ // #664 S3: rgba(59,130,246,…) → --glow-blue-rgb (tokens.css). Değer AYNI
117
+ // (Tailwind blue-500), yalnız merkezi token'a taşındı — görsel değişiklik yok.
118
+ "shadow-[0_0_30px_rgba(var(--glow-blue-rgb),0.5)]",
119
+ "hover:shadow-[0_0_40px_rgba(var(--glow-blue-rgb),0.7)]",
120
+ "active:shadow-[0_0_20px_rgba(var(--glow-blue-rgb),0.4)]",
109
121
  "hover:bg-blue-600",
110
122
  "focus-visible:ring-blue-500/50",
111
123
  "transition-all duration-300",
@@ -134,8 +146,9 @@ const buttonVariants = cva(
134
146
  "bg-transparent text-green-400",
135
147
  "border-2 border-green-400",
136
148
  "hover:bg-green-400 hover:text-gray-900",
137
- "hover:shadow-[0_0_30px_rgba(74,222,128,0.6)]",
138
- "active:shadow-[0_0_15px_rgba(74,222,128,0.4)]",
149
+ // #664 S3: rgba(74,222,128,…) → --glow-green-rgb (tokens.css), piksel-özdeş.
150
+ "hover:shadow-[0_0_30px_rgba(var(--glow-green-rgb),0.6)]",
151
+ "active:shadow-[0_0_15px_rgba(var(--glow-green-rgb),0.4)]",
139
152
  "focus-visible:ring-green-400/50",
140
153
  "transition-all duration-300",
141
154
  ],
@@ -191,14 +204,21 @@ export interface ButtonProps
191
204
  * @param props.rightIcon - Icon on the right side
192
205
  * @param props.fullWidth - Full width display
193
206
  * @param props.rounded - Corner rounding style (default, full, none)
194
- * @param props.asChild - For Radix UI polymorphic component
207
+ * @param props.asChild - Renders the single child element instead of a native
208
+ * `<button>` (Radix `Slot`), e.g. `<Button asChild><a href="/x">Go</a></Button>`.
209
+ * The `buttonVariants` classes, the loading spinner and `leftIcon`/`rightIcon`
210
+ * are merged INTO that child. Because non-button elements ignore the native
211
+ * `disabled` attribute, `disabled`/`loading` are expressed as `aria-disabled` +
212
+ * `data-disabled` plus inert styling, and activation is blocked in the click
213
+ * handler. Falls back to a native `<button>` when the child is not a valid
214
+ * React element.
195
215
  */
196
216
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
197
- ({
198
- className,
199
- variant,
200
- size,
201
- rounded,
217
+ ({
218
+ className,
219
+ variant,
220
+ size,
221
+ rounded,
202
222
  fullWidth,
203
223
  asChild = false,
204
224
  loading = false,
@@ -206,27 +226,74 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
206
226
  rightIcon,
207
227
  children,
208
228
  disabled,
209
- ...props
229
+ onClick,
230
+ ...props
210
231
  }, ref) => {
211
- // Radix UI'ın asChild pattern'i için - "as" prop yerine
212
- const Comp = asChild ? "button" : "button";
213
-
232
+ // #649: `asChild` daha önce `asChild ? "button" : "button"` idi — ternary'nin iki
233
+ // dalı BİREBİR aynıydı, yani prop tipte ve docs'ta vaat edilmesine rağmen hiçbir
234
+ // şey yapmıyordu. Radix `Slot`a bağlandı (kardeş desen: breadcrumb #309, card #310).
235
+ //
236
+ // Slot TEK ve GEÇERLİ bir element çocuğu ister. `asChild` bugüne dek etkisiz
237
+ // olduğu için `<Button asChild>düz metin</Button>` yazan mevcut kullanım sorunsuz
238
+ // çalışıyordu; doğrudan Slot'a düşseydi sessizce BOŞ render ederdi. Bu yüzden
239
+ // çocuk element değilse native <button>'a geri düşülür.
240
+ const renderAsSlot = asChild && React.isValidElement(children);
241
+ const Comp = renderAsSlot ? Slot : "button";
242
+ const isDisabled = disabled || loading;
243
+
244
+ // asChild modunda çocuk <a>/<Link>/<span> olabilir; bu elemanlar native `disabled`
245
+ // özniteliğini TANIMAZ:
246
+ // • React onu geçersiz bir öznitelik olarak DOM'a basar,
247
+ // • `:disabled` hiç eşleşmez → cva'daki `disabled:*` stilleri ölü kalır,
248
+ // • yardımcı teknoloji "devre dışı" duyurusunu alamaz.
249
+ // Bu yüzden devre dışılık Slot dalında ARIA + veri özniteliği ile ifade edilir;
250
+ // native `disabled` yalnız gerçek <button> dalında verilir.
251
+ const disabledProps = renderAsSlot
252
+ ? {
253
+ "aria-disabled": isDisabled || undefined,
254
+ "data-disabled": isDisabled ? "" : undefined,
255
+ }
256
+ : { disabled: isDisabled };
257
+
258
+ // asChild + devre dışı: `pointer-events-none` fareyi keser ama bir <a> klavyeden
259
+ // (Enter) hâlâ etkinleşir. Etkinleştirme burada durdurulur; `preventDefault`
260
+ // bağlantı gezinmesini de iptal eder.
261
+ // SINIR: Radix `Slot` olay işleyicilerini birleştirirken ÖNCE çocuğun kendi
262
+ // işleyicisini çağırır — çocuğa doğrudan bağlanmış bir `onClick` engellenemez.
263
+ const handleClick =
264
+ renderAsSlot && isDisabled
265
+ ? (event: React.MouseEvent<HTMLButtonElement>) => {
266
+ event.preventDefault();
267
+ event.stopPropagation();
268
+ }
269
+ : onClick;
270
+
214
271
  return (
215
272
  <Comp
216
- className={cn("moonui-theme", buttonVariants({ variant, size, rounded, fullWidth, className }))}
273
+ className={cn(
274
+ "moonui-theme",
275
+ buttonVariants({ variant, size, rounded, fullWidth, className }),
276
+ // `:disabled` eşleşmediği için görsel/etkileşimsel devre dışılık Slot
277
+ // dalında açık sınıflarla verilir.
278
+ renderAsSlot && isDisabled && "opacity-50 cursor-not-allowed pointer-events-none"
279
+ )}
217
280
  ref={ref}
218
- disabled={disabled || loading}
281
+ {...disabledProps}
219
282
  // Görsel spinner + data-loading vardı ama yardımcı teknolojiye meşgul
220
283
  // durumu hiç duyurulmuyordu.
221
284
  aria-busy={loading || undefined}
222
285
  data-loading={loading ? "" : undefined}
223
286
  {...props}
287
+ onClick={handleClick}
224
288
  >
225
289
  {loading && (
226
290
  <Loader2 className="w-4 h-4 animate-spin" aria-hidden="true" />
227
291
  )}
228
292
  {!loading && leftIcon}
229
- {children}
293
+ {/* #649: Slot dalında spinner ve ikonlar `Slottable` sayesinde çocuğun İÇİNE
294
+ taşınır; aksi hâlde Slot birden çok çocuk görüp `Children.only` ile patlardı
295
+ ve `loading`/ikon sözleşmesi asChild'da tamamen kaybolurdu. */}
296
+ {renderAsSlot ? <Slottable>{children}</Slottable> : children}
230
297
  {!loading && rightIcon}
231
298
  </Comp>
232
299
  );
@@ -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"