@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.
- 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 +448 -93
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +449 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/issue-544-kontrast.test.tsx +609 -0
- 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__/badge.test.tsx +5 -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 +31 -4
- package/src/components/ui/button.tsx +88 -21
- 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
|
@@ -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:
|
|
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
|
|
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:
|
|
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-
|
|
73
|
-
"hover:bg-destructive/90 dark:hover:bg-
|
|
74
|
-
"active:bg-destructive/80 dark:active:bg-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
"
|
|
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
|
-
|
|
138
|
-
"
|
|
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 -
|
|
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
|
-
|
|
229
|
+
onClick,
|
|
230
|
+
...props
|
|
210
231
|
}, ref) => {
|
|
211
|
-
//
|
|
212
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
{
|
|
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-
|
|
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"
|