@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.
- package/dist/cdn/moonui.css +2 -2
- package/dist/cdn/moonui.esm.js +12 -12
- package/dist/cdn/moonui.global.js +12 -12
- package/dist/index.d.mts +24 -6
- package/dist/index.d.ts +24 -6
- package/dist/index.js +436 -93
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +437 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/issue-544-kontrast.test.tsx +153 -145
- package/src/__tests__/issue-645-overlay-tab-focus.test.tsx +262 -0
- package/src/__tests__/issue-646-dark-scope-override.test.tsx +297 -0
- package/src/__tests__/issue-661-a11y.test.tsx +113 -0
- package/src/__tests__/issue-661-token-kontrast.test.tsx +82 -0
- package/src/components/ui/__tests__/accent-foreground-contrast.test.ts +197 -0
- package/src/components/ui/__tests__/accordion.test.tsx +6 -4
- package/src/components/ui/__tests__/alert.test.tsx +4 -1
- package/src/components/ui/__tests__/button.test.tsx +6 -1
- package/src/components/ui/__tests__/dark-token-contrast.test.ts +18 -18
- package/src/components/ui/__tests__/heading-level-policy.test.tsx +4 -4
- package/src/components/ui/__tests__/issue-643-card-token-parite.test.tsx +113 -0
- package/src/components/ui/__tests__/issue-649-button-aschild.test.tsx +199 -0
- package/src/components/ui/__tests__/issue-664-separator-dekoratif-rol.test.tsx +41 -0
- package/src/components/ui/__tests__/preset-color-shadowing.test.ts +10 -10
- package/src/components/ui/__tests__/primary-subtle-token.test.ts +7 -7
- package/src/components/ui/__tests__/progress-secondary-contrast.test.ts +38 -38
- package/src/components/ui/__tests__/progress-warning-error-contrast.test.ts +64 -64
- package/src/components/ui/__tests__/quality-group-de.test.tsx +2 -2
- package/src/components/ui/__tests__/responsive-color-invariance.test.ts +20 -20
- package/src/components/ui/__tests__/select.test.tsx +6 -1
- package/src/components/ui/__tests__/semantic-token-usage.test.ts +23 -23
- package/src/components/ui/__tests__/size-scale-convention.test.ts +10 -10
- package/src/components/ui/__tests__/slider-accent-contrast.test.ts +142 -136
- package/src/components/ui/__tests__/slider-secondary-contrast.test.ts +60 -60
- package/src/components/ui/__tests__/slider-warning-error-contrast.test.ts +100 -100
- package/src/components/ui/__tests__/switch-variant-contrast.test.tsx +103 -103
- package/src/components/ui/__tests__/tooltip.test.tsx +122 -0
- package/src/components/ui/accordion.tsx +16 -3
- package/src/components/ui/alert.tsx +1 -1
- package/src/components/ui/badge.tsx +12 -1
- package/src/components/ui/button.tsx +95 -24
- package/src/components/ui/card.tsx +59 -5
- package/src/components/ui/dropdown-menu.tsx +99 -5
- package/src/components/ui/file-upload.tsx +4 -0
- package/src/components/ui/index.ts +4 -1
- package/src/components/ui/popover-pro.tsx +41 -0
- package/src/components/ui/popover.tsx +162 -7
- package/src/components/ui/rating.tsx +24 -2
- package/src/components/ui/select.tsx +4 -4
- package/src/components/ui/simple-editor.tsx +3 -18
- package/src/components/ui/tabs.tsx +16 -1
- package/src/components/ui/textarea.tsx +6 -1
- package/src/components/ui/toast.tsx +17 -2
- package/src/components/ui/tooltip.tsx +154 -2
- 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
|
|
92
|
-
"active:bg-success/80 dark:active:bg-success
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
"
|
|
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 -
|
|
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
|
-
|
|
242
|
+
onClick,
|
|
243
|
+
...props
|
|
219
244
|
}, ref) => {
|
|
220
|
-
//
|
|
221
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
{
|
|
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-
|
|
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:
|
|
31
|
-
filled: "border-none bg-secondary dark:bg-
|
|
32
|
-
elevated: "border-none shadow-md dark:shadow-lg dark:shadow-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|