@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
@@ -0,0 +1,82 @@
1
+ /**
2
+ * #661 — A1-A6 bundle, (C) token/kontrast dilimi · free paket tarafı
3
+ *
4
+ * Kalem 14 · `textarea` — karakter sayacı `text-muted-foreground` ÜSTÜNE
5
+ * `dark:text-gray-500` bindiriyordu. Token zaten doğruydu; ikinci katman
6
+ * koyu temada onu AA altına indiriyordu (#646 ile aynı kusur sınıfı).
7
+ * Ölçüldü (kart zemini #111827): 3.68:1 → 6.29:1. Açık tema değişmez.
8
+ *
9
+ * Kalem 17 · `accordion` — free ikiz ham `gray-*` kullanıyordu, Pro ikizi
10
+ * `border-border`/`text-muted-foreground` token'larını kullanıyordu (ters
11
+ * parite). Ham palet marka/tema özelleştirmesine KAPALIDIR: tüketicinin
12
+ * `--border`i değiştirmesi accordion kenarlığına hiç ulaşmıyordu.
13
+ * Ölçüldü: içerik metni açık 10.30 → 11.48, koyu 12.19 → 9.44 (ikisi de
14
+ * AA'nın çok üstünde — eşik geçilmiyor, #545 "ihlal takası" yok).
15
+ *
16
+ * ÖLÇÜM DİSİPLİNİ: iddia kaynak metninden DEĞİL render edilmiş elemanın
17
+ * `class` özniteliğinden okunur. jsdom CSS uygulamaz ama sınıfın DOM'a
18
+ * BASILDIĞI (ve ham sınıfın basılMADIĞI) ölçülebilir bir olgudur.
19
+ */
20
+ import * as React from 'react'
21
+ import { render, screen } from '@testing-library/react'
22
+ import '@testing-library/jest-dom'
23
+
24
+ import { Textarea } from '../components/ui/textarea'
25
+ import {
26
+ Accordion,
27
+ AccordionItem,
28
+ AccordionTrigger,
29
+ AccordionContent,
30
+ } from '../components/ui/accordion'
31
+
32
+ /* ────────────────────────────────────────────────────────────────────────── *
33
+ * Kalem 14 · textarea karakter sayacı
34
+ * ────────────────────────────────────────────────────────────────────────── */
35
+ describe('#661-14 · Textarea karakter sayacı ikinci renk katmanı taşımıyor', () => {
36
+ it('sayaç `text-muted-foreground` kullanır, `dark:text-gray-500` BASILMAZ', () => {
37
+ render(<Textarea characterCount maxLength={100} defaultValue="moonui" aria-label="Notes" />)
38
+
39
+ const counter = screen.getByText('6 / 100')
40
+ expect(counter.className).toContain('text-muted-foreground')
41
+ // Kusurlu sürümde bu sınıf DA basılıyordu ve `dark:` özgüllüğüyle kazanıyordu.
42
+ expect(counter.className).not.toContain('dark:text-gray-500')
43
+ })
44
+ })
45
+
46
+ /* ────────────────────────────────────────────────────────────────────────── *
47
+ * Kalem 17 · accordion ham palet → token
48
+ * ────────────────────────────────────────────────────────────────────────── */
49
+ describe('#661-17 · Accordion ham Tailwind paleti yerine token kullanır', () => {
50
+ function renderAccordion() {
51
+ return render(
52
+ <Accordion type="single" defaultValue="a" collapsible>
53
+ <AccordionItem value="a" data-testid="item">
54
+ <AccordionTrigger>Trigger label</AccordionTrigger>
55
+ <AccordionContent data-testid="content">Body copy</AccordionContent>
56
+ </AccordionItem>
57
+ </Accordion>
58
+ )
59
+ }
60
+
61
+ it('AccordionItem kenarlığı `border-border` — ham gray YOK', () => {
62
+ renderAccordion()
63
+ const item = screen.getByTestId('item')
64
+ expect(item.className).toContain('border-border')
65
+ expect(item.className).not.toMatch(/border-gray-\d/)
66
+ expect(item.className).not.toMatch(/dark:border-gray-\d/)
67
+ })
68
+
69
+ it('AccordionTrigger koyu tema metin katmanı taşımaz (foreground miras alınır)', () => {
70
+ renderAccordion()
71
+ const trigger = screen.getByRole('button', { name: 'Trigger label' })
72
+ expect(trigger.className).not.toMatch(/dark:text-gray-\d/)
73
+ })
74
+
75
+ it('AccordionContent `text-foreground/80` — ham gray çifti YOK', () => {
76
+ renderAccordion()
77
+ const content = screen.getByTestId('content')
78
+ expect(content.className).toContain('text-foreground/80')
79
+ expect(content.className).not.toMatch(/text-gray-\d/)
80
+ expect(content.className).not.toMatch(/dark:text-gray-\d/)
81
+ })
82
+ })
@@ -0,0 +1,197 @@
1
+ /**
2
+ * #647 — `--accent` / `--accent-foreground` çifti LIGHT temada AA altındaydı.
3
+ *
4
+ * BULGU: `--accent-foreground` (metin) 51% idi; zemin `--accent` (94%) ile birlikte
5
+ * 4.26:1 veriyordu (WCAG AA küçük/normal metin eşiği 4.5:1 — WCAG 1.4.3). Bu koşuda
6
+ * DÖRT bağımsız yüzeyde, üç ayrı batch'te AYNI rakamla ölçüldü:
7
+ * 1. `toggle-group` aktif öğe — toggle.tsx `data-[state=on]:bg-accent
8
+ * data-[state=on]:text-accent-foreground`
9
+ * 2. `command` / `command-pro` seçili satır — command.tsx `aria-selected:bg-accent
10
+ * aria-selected:text-accent-foreground`
11
+ * 3. `navbar` aktif öğe — moonui-pro navbar/index.tsx `bg-accent
12
+ * text-accent-foreground`
13
+ * 4. `magnetic-button` outline/ghost hover — moonui-pro magnetic-button.tsx inline
14
+ * `backgroundColor: hsl(var(--accent))` + `color: hsl(var(--accent-foreground))`
15
+ *
16
+ * ⚠️ Bu kalem #606'nın kapsamındaydı ama #606 KAPANDI — borç kapalı issue'ya
17
+ * havale edilemediği için #647 olarak ayrı açıldı.
18
+ *
19
+ * KÖK NEDEN / ÇÖZÜM SINIFI: `--accent` (94%) `--secondary` (94%) ile BİREBİR aynı
20
+ * zemin; eski `--accent-foreground` (51%) de eski `--secondary-foreground` ile
21
+ * BİREBİR aynı değerdi. #284 `--secondary-foreground`'u aynı zeminde 51%→49%
22
+ * yaparak 4.54'e taşımıştı — aynı zemin + aynı başlangıç L olduğu için BİREBİR
23
+ * aynı düzeltme burada da 4.54 verir. `--accent` (zemin rolü) DEĞİŞMEDİ: dört
24
+ * yüzeyin hepsi `--accent`'i zemin, `--accent-foreground`'u metin olarak kullanıyor,
25
+ * yani rol korunarak yalnız metin koyulaştırıldı. Koyu tema zaten 14.11:1 →
26
+ * dokunulmadı (bkz. `#448'in kabul kriteri` slider-accent-contrast.test.ts).
27
+ *
28
+ * NOT: `slider-accent-contrast.test.ts` (#448) `--accent`'in DOLGU (fill) rolünde
29
+ * yüzey token'ı olarak neden kullanılamadığını kilitler — o dosyanın ekseni
30
+ * FARKLI (non-text 3:1, `bg-accent` kendi zeminine bitişik). Bu dosya METİN
31
+ * eksenini (4.5:1, `--accent-foreground` üstteki mürekkep) kilitler.
32
+ * Pro tarafında ayrı bir kopya YOK: pro'nun kendi token seti yok (#415), aynı
33
+ * tokens.css'ten okur — tüketici-yüzey kilidi aşağıda pro kaynağını da tarar.
34
+ */
35
+ import * as fs from 'fs'
36
+ import * as path from 'path'
37
+
38
+ const KOK = path.resolve(__dirname, '../../..')
39
+ const PRO_KOK = path.resolve(__dirname, '../../../../../moonui-pro/src')
40
+ const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
41
+
42
+ /** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
43
+ function tokenOku(name: string, blok: 'light' | 'dark'): string | null {
44
+ const source =
45
+ blok === 'dark'
46
+ ? tokens.slice(tokens.indexOf('.dark'))
47
+ : tokens.slice(0, tokens.indexOf('.dark'))
48
+ return source.match(new RegExp(`--${name}:\\s*([^;]+);`))?.[1].trim() ?? null
49
+ }
50
+
51
+ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
52
+ h /= 360
53
+ s /= 100
54
+ l /= 100
55
+ const f = (n: number) => {
56
+ const k = (n + h * 12) % 12
57
+ const a = s * Math.min(l, 1 - l)
58
+ return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
59
+ }
60
+ return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
61
+ }
62
+
63
+ const hslTokenRgb = (v: string): [number, number, number] => {
64
+ const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
65
+ return hslToRgb(h, s, l)
66
+ }
67
+
68
+ function relativeLuminance([r, g, b]: [number, number, number]): number {
69
+ const c = (v: number) => {
70
+ const x = v / 255
71
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
72
+ }
73
+ return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
74
+ }
75
+
76
+ function rgbKontrast(a: [number, number, number], b: [number, number, number]): number {
77
+ const la = relativeLuminance(a)
78
+ const lb = relativeLuminance(b)
79
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la]
80
+ return (hi + 0.05) / (lo + 0.05)
81
+ }
82
+
83
+ /** `bg-accent/NN` gibi alfa katmanının zemin üstündeki bileşke rengi */
84
+ function alfaBileske(
85
+ ust: [number, number, number],
86
+ alfa: number,
87
+ background: [number, number, number]
88
+ ): [number, number, number] {
89
+ return [
90
+ Math.round(alfa * ust[0] + (1 - alfa) * background[0]),
91
+ Math.round(alfa * ust[1] + (1 - alfa) * background[1]),
92
+ Math.round(alfa * ust[2] + (1 - alfa) * background[2]),
93
+ ]
94
+ }
95
+
96
+ const AA_ESIK = 4.5
97
+
98
+ describe('#647 hesaplayıcı çapası', () => {
99
+ it('ESKİ değer (51%) BUGÜN BİLE hesaplansa 4.5 altı kalır — bug belgesi', () => {
100
+ // #647 gövdesinde rapor edilen 4.26 ölçümünü yeniden üretir (tokens.css'ten
101
+ // değil, literal'den — dosya artık düzeltilmiş değeri taşıyor).
102
+ const eskiZemin = hslTokenRgb(tokenOku('accent', 'light')!) // --accent DEĞİŞMEDİ
103
+ const eskiMetin = hslToRgb(217.2, 91.2, 51)
104
+ const oran = rgbKontrast(eskiZemin, eskiMetin)
105
+ expect(oran).toBeGreaterThan(4.2)
106
+ expect(oran).toBeLessThan(4.3)
107
+ })
108
+ })
109
+
110
+ describe('#647 --accent / --accent-foreground — LIGHT AA (4.5:1)', () => {
111
+ it('tokens.css BUGÜNKÜ light çifti AA eşiğini GEÇİYOR', () => {
112
+ const zemin = hslTokenRgb(tokenOku('accent', 'light')!)
113
+ const metin = hslTokenRgb(tokenOku('accent-foreground', 'light')!)
114
+ const oran = rgbKontrast(zemin, metin)
115
+ expect(oran).toBeGreaterThanOrEqual(AA_ESIK)
116
+ // Sıkı bant: gelecekte biri değeri sessizce kaydırırsa (ör. 48%→50%) burası
117
+ // erken uyarır — #284'ün --secondary-foreground düzeltmesiyle AYNI sonucu
118
+ // (4.54) üretmesi, iki tokenin aynı zeminden geldiğinin çapraz kanıtıdır.
119
+ expect(oran).toBeGreaterThan(4.5)
120
+ expect(oran).toBeLessThan(4.6)
121
+ })
122
+
123
+ it('`--accent` (zemin rolü) DOKUNULMADI — yalnız metin koyulaştı', () => {
124
+ expect(tokenOku('accent', 'light')).toBe('217.2 32.6% 94%')
125
+ expect(tokenOku('accent-foreground', 'light')).toBe('217.2 91.2% 49%')
126
+ })
127
+
128
+ it('alfa-katmanlı tüketiciler (bg-accent/NN + text-accent-foreground) DE geçiyor', () => {
129
+ // calendar.tsx `bg-accent/30 text-accent-foreground`, pro toggle.tsx
130
+ // `bg-accent/20|60 text-accent-foreground` gibi kısmi-opaklık kombinasyonları.
131
+ // Zemin --accent (94%) --background'dan (100%) daha koyu olduğu için alfa
132
+ // düştükçe bileşke DAHA açık bir griye kayar — bu da koyu metinle kontrastı
133
+ // ARTIRIR, asla düşürmez. Bu test o yönün korunduğunu kilitler.
134
+ const background = hslTokenRgb(tokenOku('background', 'light')!)
135
+ const accentZemin = hslTokenRgb(tokenOku('accent', 'light')!)
136
+ const metin = hslTokenRgb(tokenOku('accent-foreground', 'light')!)
137
+ const alfalar = [1, 0.6, 0.5, 0.3, 0.2]
138
+ const oranlar = alfalar.map((a) =>
139
+ rgbKontrast(alfaBileske(accentZemin, a, background), metin)
140
+ )
141
+ for (const oran of oranlar) expect(oran).toBeGreaterThanOrEqual(AA_ESIK)
142
+ // Monoton: alfa azaldıkça oran azalmıyor (zemin beyaza yaklaştığı için).
143
+ for (let i = 1; i < oranlar.length; i++) {
144
+ expect(oranlar[i]).toBeGreaterThanOrEqual(oranlar[i - 1] - 1e-9)
145
+ }
146
+ })
147
+ })
148
+
149
+ describe('#647 DARK tema — negatif kontrol (dokunulmadı)', () => {
150
+ it('dark `--accent` / `--accent-foreground` değerleri BİREBİR KORUNDU', () => {
151
+ expect(tokenOku('accent', 'dark')).toBe('216 34% 17%')
152
+ expect(tokenOku('accent-foreground', 'dark')).toBe('210 40% 98%')
153
+ })
154
+
155
+ it('dark çift zaten AA üstündeydi ve ÖYLE KALDI (yeni ihlal doğmadı)', () => {
156
+ const zemin = hslTokenRgb(tokenOku('accent', 'dark')!)
157
+ const metin = hslTokenRgb(tokenOku('accent-foreground', 'dark')!)
158
+ const oran = rgbKontrast(zemin, metin)
159
+ expect(oran).toBeGreaterThan(13) // ölçülen ~14.11
160
+ })
161
+ })
162
+
163
+ describe('#647 tüketici-yüzey kilidi — dört yüzey hâlâ HAM çifti kullanıyor', () => {
164
+ // Bu testler token düzeltmesinin VARSAYIMINI kilitler: dört yüzey `--accent` /
165
+ // `--accent-foreground`'u DOĞRUDAN (className veya inline hsl(var(...))) tüketiyor,
166
+ // yerel bir renk override'ı YOK. Biri ileride bu sınıfları/inline stilleri
167
+ // değiştirirse (component kaynağı bu PR'ın kapsamı DIŞI) burası kırmızı olur ve
168
+ // token düzeltmesinin o yüzeyi artık kapsamadığını haber verir.
169
+ it('toggle-group (free `toggle.tsx`): data-[state=on] çifti sınıf olarak duruyor', () => {
170
+ const src = fs.readFileSync(path.join(KOK, 'components/ui/toggle.tsx'), 'utf8')
171
+ expect(src).toMatch(/data-\[state=on\]:bg-accent\s+data-\[state=on\]:text-accent-foreground/)
172
+ })
173
+
174
+ it('command (free `command.tsx`): aria-selected çifti sınıf olarak duruyor', () => {
175
+ const src = fs.readFileSync(path.join(KOK, 'components/ui/command.tsx'), 'utf8')
176
+ expect(src).toMatch(/aria-selected:bg-accent\s+aria-selected:text-accent-foreground/)
177
+ })
178
+
179
+ it('command-pro (`command.tsx`): aria-selected çifti sınıf olarak duruyor', () => {
180
+ const src = fs.readFileSync(path.join(PRO_KOK, 'components/ui/command.tsx'), 'utf8')
181
+ expect(src).toMatch(/aria-selected:bg-accent\s+aria-selected:text-accent-foreground/)
182
+ })
183
+
184
+ it('navbar (pro `navbar/index.tsx`): aktif öğe çifti sınıf olarak duruyor', () => {
185
+ const src = fs.readFileSync(path.join(PRO_KOK, 'components/navbar/index.tsx'), 'utf8')
186
+ expect(src).toMatch(/bg-accent\s+text-accent-foreground/)
187
+ })
188
+
189
+ it('magnetic-button (pro): outline/ghost inline hsl(var(...)) çifti duruyor', () => {
190
+ const src = fs.readFileSync(
191
+ path.join(PRO_KOK, 'components/ui/magnetic-button.tsx'),
192
+ 'utf8'
193
+ )
194
+ expect(src).toContain("backgroundColor: 'hsl(var(--accent))'")
195
+ expect(src).toContain("color: 'hsl(var(--accent-foreground))'")
196
+ })
197
+ })
@@ -135,8 +135,9 @@ describe('Accordion Components', () => {
135
135
 
136
136
  const item = screen.getByTestId('accordion-item')
137
137
  expect(item).toHaveClass('border-b')
138
- expect(item).toHaveClass('border-gray-200')
139
- expect(item).toHaveClass('dark:border-gray-800')
138
+ // #661-17: ham `border-gray-200 dark:border-gray-800` → `border-border`
139
+ // (Pro ikizi zaten token kullanıyordu; token değerleri görsel olarak denk).
140
+ expect(item).toHaveClass('border-border')
140
141
  })
141
142
 
142
143
  it('forwards ref correctly', () => {
@@ -349,8 +350,9 @@ describe('Accordion Components', () => {
349
350
  const content = screen.getByTestId('accordion-content')
350
351
  expect(content).toHaveClass('overflow-hidden')
351
352
  expect(content).toHaveClass('text-sm')
352
- expect(content).toHaveClass('text-gray-700')
353
- expect(content).toHaveClass('dark:text-gray-300')
353
+ // #661-17: ham `text-gray-700 dark:text-gray-300` → `text-foreground/80`
354
+ // (Pro ikiziyle parite; ölçüm accordion.tsx içindeki yorumda).
355
+ expect(content).toHaveClass('text-foreground/80')
354
356
  })
355
357
 
356
358
  it('has animation classes', () => {
@@ -60,7 +60,10 @@ describe('Alert Components', () => {
60
60
  render(<Alert variant="warning" data-testid="alert">Test</Alert>)
61
61
  const alert = screen.getByTestId('alert')
62
62
  expect(alert).toHaveClass('bg-warning/10')
63
- expect(alert).toHaveClass('text-warning')
63
+ // #630: metin `text-warning` idi ve `bg-warning/10` zemininde 1.98:1 ölçüldü —
64
+ // AA'nın (4.5) çok altında. Yüzey rengi (`--warning`) DEĞİŞMEDİ; yalnız metin
65
+ // aynı ailenin yüzey-üstü tonuna taşındı: 8.65 (light) / 15.71 (dark).
66
+ expect(alert).toHaveClass('text-warning-subtle-foreground')
64
67
  expect(alert).toHaveClass('border-warning/30')
65
68
  })
66
69
 
@@ -41,9 +41,13 @@ describe('Badge Component', () => {
41
41
  })
42
42
 
43
43
  it('renders destructive variant correctly', () => {
44
+ // #544: dolgu `bg-error` (--error = #ef4343, beyaz metinle 3.78 ✗) yerine
45
+ // `error` skalasının -600 kademesine (#dc2626, 4.83 ✓) bağlandı. Token
46
+ // DEĞİŞMEDİ; yalnız bu varyantın dolgusu AA'yı geçen en açık kademeye indi.
47
+ // Kontrast değişmezleri: packages/moonui/src/__tests__/issue-544-kontrast.test.tsx
44
48
  render(<Badge variant="destructive">Destructive</Badge>)
45
49
  const badge = screen.getByText('Destructive').parentElement!
46
- expect(badge).toHaveClass('bg-error')
50
+ expect(badge).toHaveClass('bg-error-600')
47
51
  expect(badge).toHaveClass('text-white')
48
52
  })
49
53
 
@@ -75,10 +75,15 @@ describe('Button Component', () => {
75
75
  <a href="/test">Link Button</a>
76
76
  </Button>
77
77
  )
78
-
78
+
79
79
  const link = screen.getByRole('link')
80
80
  expect(link).toBeInTheDocument()
81
81
  expect(link).toHaveAttribute('href', '/test')
82
+ // #649: bu satır olmadan test YANLIŞ NEDENLE geçiyordu — `asChild` ölü bir
83
+ // ternary'ydi ve <a>, native <button>'ın İÇİNE render ediliyordu. Slot dalının
84
+ // ayırt edici iddiası: <button> hiç üretilmemeli. Ayrıntı:
85
+ // __tests__/issue-649-button-aschild.test.tsx
86
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
82
87
  })
83
88
 
84
89
  it('applies custom className', () => {
@@ -32,15 +32,15 @@ const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
32
32
  * #284 konvansiyonu: kenarlık/metin pratikte `--background` (4%) üstünde değil
33
33
  * bu yüzeyde duruyor; kontrast gerçek zemine göre ölçülür (tokens.css --input notu).
34
34
  */
35
- const KART_GERCEK: [number, number, number] = [17, 24, 39]
35
+ const CARD_ACTUAL: [number, number, number] = [17, 24, 39]
36
36
 
37
37
  /** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
38
- function tokenOku(ad: string, blok: 'light' | 'dark'): string | null {
39
- const kaynak =
38
+ function tokenOku(name: string, blok: 'light' | 'dark'): string | null {
39
+ const source =
40
40
  blok === 'dark'
41
41
  ? tokens.slice(tokens.indexOf('.dark'))
42
42
  : tokens.slice(0, tokens.indexOf('.dark'))
43
- return kaynak.match(new RegExp(`--${ad}:\\s*([^;]+);`))?.[1].trim() ?? null
43
+ return source.match(new RegExp(`--${name}:\\s*([^;]+);`))?.[1].trim() ?? null
44
44
  }
45
45
 
46
46
  function hslToRgb(h: number, s: number, l: number): [number, number, number] {
@@ -55,7 +55,7 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
55
55
  return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
56
56
  }
57
57
 
58
- function bagilParlaklik([r, g, b]: [number, number, number]): number {
58
+ function relativeLuminance([r, g, b]: [number, number, number]): number {
59
59
  const c = (v: number) => {
60
60
  const x = v / 255
61
61
  return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
@@ -64,23 +64,23 @@ function bagilParlaklik([r, g, b]: [number, number, number]): number {
64
64
  }
65
65
 
66
66
  function rgbKontrast(a: [number, number, number], b: [number, number, number]): number {
67
- const la = bagilParlaklik(a)
68
- const lb = bagilParlaklik(b)
67
+ const la = relativeLuminance(a)
68
+ const lb = relativeLuminance(b)
69
69
  const [hi, lo] = la > lb ? [la, lb] : [lb, la]
70
70
  return (hi + 0.05) / (lo + 0.05)
71
71
  }
72
72
 
73
- const ayrıştır = (v: string): [number, number, number] => {
73
+ const parse = (v: string): [number, number, number] => {
74
74
  const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
75
75
  return hslToRgb(h, s, l)
76
76
  }
77
77
 
78
78
  /** İki token (veya token + sabit RGB yüzey) arasındaki kontrast oranı */
79
79
  function kontrast(a: string, b: string | [number, number, number]): number {
80
- return rgbKontrast(ayrıştır(a), Array.isArray(b) ? b : ayrıştır(b))
80
+ return rgbKontrast(parse(a), Array.isArray(b) ? b : parse(b))
81
81
  }
82
82
 
83
- const dark = (ad: string) => tokenOku(ad, 'dark')!
83
+ const dark = (name: string) => tokenOku(name, 'dark')!
84
84
 
85
85
  describe('#368 koyu tema token kontrastları', () => {
86
86
  it('hesaplayıcı DOĞRU — bilinen bir çift belgelenen oranı veriyor', () => {
@@ -93,7 +93,7 @@ describe('#368 koyu tema token kontrastları', () => {
93
93
 
94
94
  describe('--destructive: koyu temada METİN rolü (asıl bulgu, eski değer 1.79)', () => {
95
95
  it('gerçek kart zemini (#111827) üstünde AA geçiyor (>=4.5)', () => {
96
- expect(kontrast(dark('destructive'), KART_GERCEK)).toBeGreaterThanOrEqual(4.5)
96
+ expect(kontrast(dark('destructive'), CARD_ACTUAL)).toBeGreaterThanOrEqual(4.5)
97
97
  })
98
98
 
99
99
  it('--background üstünde AA geçiyor (>=4.5)', () => {
@@ -101,7 +101,7 @@ describe('#368 koyu tema token kontrastları', () => {
101
101
  })
102
102
 
103
103
  it('hata kenarlığı rolünde 1.4.11 eşiğini geçiyor (>=3.0, eski değer 1.80)', () => {
104
- expect(kontrast(dark('destructive'), KART_GERCEK)).toBeGreaterThanOrEqual(3.0)
104
+ expect(kontrast(dark('destructive'), CARD_ACTUAL)).toBeGreaterThanOrEqual(3.0)
105
105
  })
106
106
 
107
107
  it('dolgu rolü belgelenen trade-off tabanının altına DÜŞMÜYOR (fg ile >=3.0)', () => {
@@ -114,7 +114,7 @@ describe('#368 koyu tema token kontrastları', () => {
114
114
 
115
115
  describe('--muted: koyu temada dekoratif YÜZEY rolü (eski değer kartla 1.01)', () => {
116
116
  it('gerçek kart zemininden ayrışıyor (>=1.3 — Skeleton görünürlüğü)', () => {
117
- expect(kontrast(dark('muted'), KART_GERCEK)).toBeGreaterThanOrEqual(1.3)
117
+ expect(kontrast(dark('muted'), CARD_ACTUAL)).toBeGreaterThanOrEqual(1.3)
118
118
  })
119
119
 
120
120
  it('--background üstünde ayrışıyor (>=1.3)', () => {
@@ -123,7 +123,7 @@ describe('#368 koyu tema token kontrastları', () => {
123
123
 
124
124
  it('AŞIRI açılmamış — subtle doğası korunuyor (kartla <=2.0)', () => {
125
125
  // Muted "belli belirsiz" bir yüzeydir; 2.0 üstü artık vurgulu bir bloktur.
126
- expect(kontrast(dark('muted'), KART_GERCEK)).toBeLessThanOrEqual(2.0)
126
+ expect(kontrast(dark('muted'), CARD_ACTUAL)).toBeLessThanOrEqual(2.0)
127
127
  })
128
128
 
129
129
  it('--muted-foreground yeni muted zemini üstünde AA geçiyor (>=4.5, #284 deseni)', () => {
@@ -131,7 +131,7 @@ describe('#368 koyu tema token kontrastları', () => {
131
131
  })
132
132
 
133
133
  it('--muted-foreground kart ve --background üstünde AA korunuyor (>=4.5)', () => {
134
- expect(kontrast(dark('muted-foreground'), KART_GERCEK)).toBeGreaterThanOrEqual(4.5)
134
+ expect(kontrast(dark('muted-foreground'), CARD_ACTUAL)).toBeGreaterThanOrEqual(4.5)
135
135
  expect(kontrast(dark('muted-foreground'), dark('background'))).toBeGreaterThanOrEqual(4.5)
136
136
  })
137
137
  })
@@ -145,7 +145,7 @@ describe('#368 koyu tema token kontrastları', () => {
145
145
  })
146
146
 
147
147
  it('vurgu metni alternatifi canlı: --primary-subtle-foreground kart üstünde AA (>=4.5)', () => {
148
- expect(kontrast(dark('primary-subtle-foreground'), KART_GERCEK)).toBeGreaterThanOrEqual(4.5)
148
+ expect(kontrast(dark('primary-subtle-foreground'), CARD_ACTUAL)).toBeGreaterThanOrEqual(4.5)
149
149
  })
150
150
  })
151
151
 
@@ -155,8 +155,8 @@ describe('#368 koyu tema token kontrastları', () => {
155
155
  ['muted', '217.2 32.6% 96%'],
156
156
  ['muted-foreground', '215.4 16.3% 45%'],
157
157
  ['primary', '217.2 91.2% 51%'],
158
- ])('--%s açık temada sabit: %s', (ad, beklenen) => {
159
- expect(tokenOku(ad, 'light')).toBe(beklenen)
158
+ ])('--%s açık temada sabit: %s', (name, expected) => {
159
+ expect(tokenOku(name, 'light')).toBe(expected)
160
160
  })
161
161
  })
162
162
  })
@@ -36,10 +36,10 @@ describe('#345 AlertTitle — varsayılan başlık DEĞİL', () => {
36
36
 
37
37
  it('görsel sınıflar `as`ten BAĞIMSIZ', () => {
38
38
  const { unmount } = render(<AlertTitle data-testid="t">B</AlertTitle>)
39
- const varsayilan = screen.getByTestId('t').className
39
+ const defaultValue = screen.getByTestId('t').className
40
40
  unmount()
41
41
  render(<AlertTitle as="h2" data-testid="t">B</AlertTitle>)
42
- expect(screen.getByTestId('t').className).toBe(varsayilan)
42
+ expect(screen.getByTestId('t').className).toBe(defaultValue)
43
43
  })
44
44
  })
45
45
 
@@ -63,9 +63,9 @@ describe('#345 CardTitle — varsayılan KORUNDU', () => {
63
63
 
64
64
  it('görsel sınıflar `as`ten BAĞIMSIZ', () => {
65
65
  const { unmount } = render(<CardTitle data-testid="t">B</CardTitle>)
66
- const varsayilan = screen.getByTestId('t').className
66
+ const defaultValue = screen.getByTestId('t').className
67
67
  unmount()
68
68
  render(<CardTitle as="div" data-testid="t">B</CardTitle>)
69
- expect(screen.getByTestId('t').className).toBe(varsayilan)
69
+ expect(screen.getByTestId('t').className).toBe(defaultValue)
70
70
  })
71
71
  })
@@ -0,0 +1,113 @@
1
+ /**
2
+ * #643 (vaka 18 — TERS PARİTE: free geride) — free `card.tsx` ham `gray-*` sınıfları
3
+ * taşırken pro ikizinde 0 taneydi. Bu suite iki şeyi birden kilitler:
4
+ * (a) hiçbir varyantın DOM'una ham gri sızmıyor (davranışsal),
5
+ * (b) iki paketin kaynağı da 0 ham gri taşıyor (parite sondası — issue'nun
6
+ * "test stratejisi" bölümünün istediği ham-renk-sınıfı sayacı).
7
+ */
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import { render, screen } from "@testing-library/react";
11
+
12
+ import { Card } from "../card";
13
+
14
+ const VARYANTLAR = ["default", "outline", "filled", "elevated", "gradient", "glass"] as const;
15
+
16
+ /** Tailwind ham gri sınıfı: isteğe bağlı değiştirici öneki + `…gray-<basamak>`. */
17
+ const HAM_GRI = /(?:^|\s)(?:[a-z-]+:)*[a-z-]*gray-\d+(?:\/\d+)?(?=\s|$)/;
18
+
19
+ /**
20
+ * Kaynaktaki yorumları atıp yalnız KOD içindeki ham gri sınıflarını sayar.
21
+ *
22
+ * SIRA KRİTİK: satır yorumları ÖNCE atılır. Tersi yapılırsa bir `//` satırının
23
+ * içindeki ardışık eğik-yıldız dizisi blok yorum başlangıcı sanılır ve tarayıcı
24
+ * gerisini (cva gövdesi dahil) yutar — bu sonda ilk yazımında tam olarak buna
25
+ * düştü ve mutasyon testi sessizce YEŞİL kaldı.
26
+ */
27
+ function hamGriSay(dosya: string): string[] {
28
+ const src = fs.readFileSync(dosya, "utf8");
29
+ const kod = src.replace(/^\s*\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
30
+ return kod.match(/[a-z-]*gray-\d+(?:\/\d+)?/g) ?? [];
31
+ }
32
+
33
+ describe("#643 vaka 18 — Card varyantlarında ham gri yok", () => {
34
+ it.each(VARYANTLAR)('variant="%s" DOM className\'inde ham gray-* taşımaz', (variant) => {
35
+ render(
36
+ <Card variant={variant} data-testid="kart">
37
+ gövde
38
+ </Card>
39
+ );
40
+ const sinif = screen.getByTestId("kart").className;
41
+ expect(sinif).not.toMatch(HAM_GRI);
42
+ });
43
+
44
+ it("taban artık kenar rengini token'dan alıyor (açık temada da)", () => {
45
+ render(<Card data-testid="kart">gövde</Card>);
46
+ const kart = screen.getByTestId("kart");
47
+ /* Eskiden taban yalnız `border` (genişlik) taşıyordu; renk Tailwind
48
+ varsayılanı `gray-200`den geliyordu — yani AÇIK tema kenarı token'ın
49
+ dışındaydı. `border-border` ikisini de token'a bağlar. */
50
+ expect(kart).toHaveClass("border");
51
+ expect(kart).toHaveClass("border-border");
52
+ expect(kart).toHaveClass("bg-card");
53
+ });
54
+
55
+ it("filled koyu tema yüzeyi ayrışan token'a bağlı (`--muted`)", () => {
56
+ render(
57
+ <Card variant="filled" data-testid="kart">
58
+ gövde
59
+ </Card>
60
+ );
61
+ const kart = screen.getByTestId("kart");
62
+ expect(kart).toHaveClass("bg-secondary");
63
+ expect(kart).toHaveClass("dark:bg-muted");
64
+ /* `--secondary` koyu temada rgb(2,8,23), sayfa zemini rgb(3,7,17) → 1.01,
65
+ yani yüzey görünmez olurdu. `dark:bg-secondary` bilinçli olarak REDDEDİLDİ. */
66
+ expect(kart).not.toHaveClass("dark:bg-secondary");
67
+ });
68
+
69
+ it("metin rengi tüketici override'ını ezen `dark:text-*` katmanı taşımıyor", () => {
70
+ render(
71
+ <Card className="text-blue-300" data-testid="kart">
72
+ gövde
73
+ </Card>
74
+ );
75
+ const sinif = screen.getByTestId("kart").className;
76
+ /* #646'nın `dark:bg-gray-900` için belgelediği tuzağın aynısı: farklı
77
+ değiştirici kümesi → tailwind-merge çakıştırmaz, `.dark` özgüllüğü kazanır
78
+ ve tüketicinin rengini sessizce ezerdi. */
79
+ expect(sinif).toContain("text-blue-300");
80
+ expect(sinif).not.toMatch(/dark:text-/);
81
+ });
82
+
83
+ it("glass ve elevated gölge/kenar renkleri nötr (gri değil)", () => {
84
+ render(
85
+ <Card variant="glass" data-testid="cam">
86
+ gövde
87
+ </Card>
88
+ );
89
+ expect(screen.getByTestId("cam")).toHaveClass("dark:border-white/10");
90
+ expect(screen.getByTestId("cam")).toHaveClass("dark:bg-black/10");
91
+ });
92
+ });
93
+
94
+ describe("#643 parite sondası — free ↔ pro ham renk sınıfı sayısı", () => {
95
+ const freeCard = path.resolve(__dirname, "../card.tsx");
96
+ const proCard = path.resolve(
97
+ __dirname,
98
+ "../../../../../moonui-pro/src/components/ui/card.tsx"
99
+ );
100
+
101
+ it("iki kaynak dosyası da mevcut (yol sondası bozulmadı)", () => {
102
+ expect(fs.existsSync(freeCard)).toBe(true);
103
+ expect(fs.existsSync(proCard)).toBe(true);
104
+ });
105
+
106
+ it("free card.tsx kodunda 0 ham gray-* sınıfı var", () => {
107
+ expect(hamGriSay(freeCard)).toEqual([]);
108
+ });
109
+
110
+ it("pro card.tsx kodunda 0 ham gray-* sınıfı var (parite korunuyor)", () => {
111
+ expect(hamGriSay(proCard)).toEqual([]);
112
+ });
113
+ });