@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
|
@@ -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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
39
|
-
const
|
|
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
|
|
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
|
|
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 =
|
|
68
|
-
const lb =
|
|
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
|
|
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(
|
|
80
|
+
return rgbKontrast(parse(a), Array.isArray(b) ? b : parse(b))
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const 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'),
|
|
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'),
|
|
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'),
|
|
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'),
|
|
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'),
|
|
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'),
|
|
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', (
|
|
159
|
-
expect(tokenOku(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
+
});
|