@moontra/moonui 6.19.0 → 6.19.2
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/index.js +185 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +185 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/progress-warning-error-contrast.test.ts +331 -0
- package/src/components/ui/__tests__/progress.test.tsx +9 -2
- package/src/components/ui/__tests__/slider-accent-contrast.test.ts +553 -0
- package/src/components/ui/__tests__/slider-warning-error-contrast.test.ts +478 -0
- package/src/components/ui/__tests__/slider.test.tsx +12 -4
- package/src/components/ui/__tests__/switch-variant-contrast.test.tsx +460 -0
- package/src/components/ui/__tests__/switch.test.tsx +11 -2
- package/src/components/ui/__tests__/table.test.tsx +222 -8
- package/src/components/ui/progress.tsx +25 -2
- package/src/components/ui/slider.tsx +92 -6
- package/src/components/ui/switch.tsx +44 -5
- package/src/components/ui/table.tsx +88 -43
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #454 — Switch `variant` (checked track) kontrast değişmezleri.
|
|
3
|
+
*
|
|
4
|
+
* BULGU (PR #453 / issue #448'in kapsam-dışı kardeş bulgusu): `secondary` varyantı
|
|
5
|
+
* checked durumda `bg-accent` kullanıyordu — ADI `secondary`, TOKEN'ı `accent`.
|
|
6
|
+
*
|
|
7
|
+
* SWITCH'TE İKİ EKSEN AYNI SAYIYI VERİR, çünkü thumb `bg-background` taşır
|
|
8
|
+
* (`switchThumbVariants` tabanı):
|
|
9
|
+
* EKSEN A — checked track ↔ sayfa/kart zemini: kontrol "açık" okunuyor mu?
|
|
10
|
+
* EKSEN B — thumb ↔ checked track: tutamacın KONUMU görülüyor mu?
|
|
11
|
+
* İkisi de aynı iki rengi karşılaştırır (`--background` ↔ track). Bu yüzden
|
|
12
|
+
* `bg-accent` ile checked switch HEM sayfadan HEM tutamacından ayrışmıyordu:
|
|
13
|
+
* light 1.154 · dark 1.359 · docs kabuğunun gerçek koyu kartı (#111827) 1.197.
|
|
14
|
+
* Switch'in tek işi durumu göstermek olduğu için bu dekoratif bir kontrast
|
|
15
|
+
* eksiği değil İŞLEVSEL bir kusurdur.
|
|
16
|
+
*
|
|
17
|
+
* Eşik 3:1 (WCAG 1.4.11, non-text UI bileşeni). axe yakalamaz: `color-contrast`
|
|
18
|
+
* kuralı yalnız METİN ölçer.
|
|
19
|
+
*
|
|
20
|
+
* İKİ AYRI KÖK NEDEN, İKİ AYRI ÇÖZÜM ŞEKLİ (ikisi de bu dosyada kilitli):
|
|
21
|
+
*
|
|
22
|
+
* 1) `secondary` — `--accent` bir YÜZEY token'ı (`hover:bg-accent` tinti) ve iki
|
|
23
|
+
* temada da kendi zeminine bitişik. Varyant kendi ailesine, `--secondary-*`
|
|
24
|
+
* slate skalasına bağlandı. #448'in slider'da uyguladığı 700/400 `dark:` ayrımı
|
|
25
|
+
* BURADA GEREKMEZ: orada 500 elenmişti çünkü slider'ın `secondary` varyantı
|
|
26
|
+
* zaten o rengi kullanıyordu (ΔE*ab = 0.0 → klon). Switch'in `accent` adlı
|
|
27
|
+
* varyantı YOK → 500 serbest ve iki temayı birden geçen TEK kademe.
|
|
28
|
+
* secondary: "data-[state=checked]:bg-[rgb(var(--secondary-500))]"
|
|
29
|
+
*
|
|
30
|
+
* 2) `warning` — KARDEŞ KUSURU, farklı sınıf (#424/PR #440 progress · #442/PR #446
|
|
31
|
+
* slider ile aynı): `--warning` tokens.css'in `.dark` bloğunda EZİLMİYOR, iki
|
|
32
|
+
* temada aynı turuncu. Dark'ta koyu zemin sayesinde geçiyor (9.446), LIGHT'ta
|
|
33
|
+
* beyaz üstünde 2.133 ✗. Emsalin şekli aynen uygulandı: light'ta -700 kademesi,
|
|
34
|
+
* dark'ta semantik token DEĞİŞMEDEN kalır (dark render bit-bit aynı).
|
|
35
|
+
* warning: "...bg-[rgb(var(--warning-700))] dark:data-[state=checked]:bg-warning"
|
|
36
|
+
*
|
|
37
|
+
* RENKLER VARSAYILMAZ — `switch.tsx`'in O ANKİ CVA dizgelerinden çözülür
|
|
38
|
+
* (PR #440/#446/#453 dersi: sabit varsayım boş-yeşil üretir).
|
|
39
|
+
*
|
|
40
|
+
* Pro ikizi: packages/moonui-pro/src/components/ui/__tests__/switch-variant-contrast.test.tsx
|
|
41
|
+
*/
|
|
42
|
+
import * as React from 'react'
|
|
43
|
+
import { render } from '@testing-library/react'
|
|
44
|
+
import '@testing-library/jest-dom'
|
|
45
|
+
import * as fs from 'fs'
|
|
46
|
+
import * as path from 'path'
|
|
47
|
+
import { Switch } from '../switch'
|
|
48
|
+
|
|
49
|
+
const KOK = path.resolve(__dirname, '../../..')
|
|
50
|
+
const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
|
|
51
|
+
|
|
52
|
+
type RGB = [number, number, number]
|
|
53
|
+
type Blok = 'light' | 'dark'
|
|
54
|
+
|
|
55
|
+
/** "H S% L%" ya da "R G B" biçimindeki ham token değeri (light = `.moonui-root`, dark = `.dark`) */
|
|
56
|
+
function tokenOku(ad: string, blok: Blok): string | null {
|
|
57
|
+
const kaynak =
|
|
58
|
+
blok === 'dark'
|
|
59
|
+
? tokens.slice(tokens.indexOf('.dark'))
|
|
60
|
+
: tokens.slice(0, tokens.indexOf('.dark'))
|
|
61
|
+
return kaynak.match(new RegExp(`--${ad}:\\s*([^;]+);`))?.[1].trim() ?? null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
65
|
+
h /= 360
|
|
66
|
+
s /= 100
|
|
67
|
+
l /= 100
|
|
68
|
+
const f = (n: number) => {
|
|
69
|
+
const k = (n + h * 12) % 12
|
|
70
|
+
const a = s * Math.min(l, 1 - l)
|
|
71
|
+
return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
|
|
72
|
+
}
|
|
73
|
+
return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const hslTokenRgb = (v: string): RGB => {
|
|
77
|
+
const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
|
|
78
|
+
return hslToRgb(h, s, l)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Skala token'ları ("--secondary-500: 100 116 139") HSL DEĞİL, RGB üçlüsüdür */
|
|
82
|
+
const rgbTokenRgb = (v: string): RGB => {
|
|
83
|
+
const p = v.split(/\s+/).map((x) => parseFloat(x))
|
|
84
|
+
return [p[0], p[1], p[2]]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Semantik token'ın ilgili temadaki değeri — `.dark` ezmesi YOKSA light'a düşer */
|
|
88
|
+
const semantikRgb = (ad: string, blok: Blok): RGB =>
|
|
89
|
+
hslTokenRgb(tokenOku(ad, blok) ?? tokenOku(ad, 'light')!)
|
|
90
|
+
|
|
91
|
+
function bagilParlaklik([r, g, b]: RGB): number {
|
|
92
|
+
const c = (v: number) => {
|
|
93
|
+
const x = v / 255
|
|
94
|
+
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
|
|
95
|
+
}
|
|
96
|
+
return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function kontrast(a: RGB, b: RGB): number {
|
|
100
|
+
const la = bagilParlaklik(a)
|
|
101
|
+
const lb = bagilParlaklik(b)
|
|
102
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
103
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* CIE76 ΔE*ab — "iki varyant AYIRT EDİLEBİLİR mi?" metriği. WCAG kontrastı bu
|
|
108
|
+
* soruya cevap veremez (yalnız bağıl parlaklık ölçer; apayrı hue'daki iki rengi
|
|
109
|
+
* "aynı" gösterebilir). Klon iddiaları bu yüzden ΔE ile kurulur. JND ≈ 2.3; eşik 10.
|
|
110
|
+
*/
|
|
111
|
+
function rgbToLab([r, g, b]: RGB): RGB {
|
|
112
|
+
const s = (v: number) => {
|
|
113
|
+
const x = v / 255
|
|
114
|
+
return (x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)) * 100
|
|
115
|
+
}
|
|
116
|
+
const [R, G, B] = [s(r), s(g), s(b)]
|
|
117
|
+
const X = R * 0.4124 + G * 0.3576 + B * 0.1805
|
|
118
|
+
const Y = R * 0.2126 + G * 0.7152 + B * 0.0722
|
|
119
|
+
const Z = R * 0.0193 + G * 0.1192 + B * 0.9505
|
|
120
|
+
const ref: RGB = [95.047, 100.0, 108.883]
|
|
121
|
+
const g2 = (t: number) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116)
|
|
122
|
+
const [fx, fy, fz] = [g2(X / ref[0]), g2(Y / ref[1]), g2(Z / ref[2])]
|
|
123
|
+
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function deltaE(a: RGB, b: RGB): number {
|
|
127
|
+
const A = rgbToLab(a)
|
|
128
|
+
const B = rgbToLab(b)
|
|
129
|
+
return Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Docs kabuğunda ölçülen GERÇEK koyu kart zemini (#284/#368/#448 konvansiyonu) */
|
|
133
|
+
const KART_GERCEK_DARK: RGB = [17, 24, 39]
|
|
134
|
+
|
|
135
|
+
function zeminler(blok: Blok): Array<[string, RGB]> {
|
|
136
|
+
const z: Array<[string, RGB]> = [
|
|
137
|
+
['--background', semantikRgb('background', blok)],
|
|
138
|
+
['--card', semantikRgb('card', blok)],
|
|
139
|
+
]
|
|
140
|
+
if (blok === 'dark') z.push(['gerçek kart #111827', KART_GERCEK_DARK])
|
|
141
|
+
return z
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ——— Renkler KAYNAKTAN türetilir (PR #440/#446/#453 dersi) ——————————————
|
|
145
|
+
const kaynak = fs.readFileSync(path.join(KOK, 'components/ui/switch.tsx'), 'utf8')
|
|
146
|
+
/** Yorum satırları ayıklanır — gerekçe metinleri `varyant:` gibi görünmesin */
|
|
147
|
+
const yorumsuz = kaynak.replace(/^[ \t]*\/\/.*$/gm, '')
|
|
148
|
+
const TRACK_CVA = yorumsuz.slice(
|
|
149
|
+
yorumsuz.indexOf('const switchVariants'),
|
|
150
|
+
yorumsuz.indexOf('const switchThumbVariants')
|
|
151
|
+
)
|
|
152
|
+
const THUMB_CVA = yorumsuz.slice(
|
|
153
|
+
yorumsuz.indexOf('const switchThumbVariants'),
|
|
154
|
+
yorumsuz.indexOf('export interface SwitchProps')
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const VARYANTLAR = ['primary', 'success', 'warning', 'destructive', 'secondary'] as const
|
|
158
|
+
type Varyant = (typeof VARYANTLAR)[number]
|
|
159
|
+
|
|
160
|
+
/** CVA `variant` bloğundaki ham sınıf dizgesi */
|
|
161
|
+
function varyantSinifi(varyant: string): string {
|
|
162
|
+
const m = TRACK_CVA.match(new RegExp(`\\n\\s*${varyant}:\\s*"([^"]+)"`))
|
|
163
|
+
if (!m) throw new Error(`${varyant} varyantı switchVariants içinde bulunamadı`)
|
|
164
|
+
return m[1]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** `bg-primary` / `bg-[rgb(var(--secondary-500))]` → RGB */
|
|
168
|
+
function sinifRengi(sinif: string, blok: Blok): RGB {
|
|
169
|
+
const skala = sinif.match(/^bg-\[rgb\(var\(--([a-z0-9-]+)\)\)\]$/)
|
|
170
|
+
if (skala) return rgbTokenRgb(tokenOku(skala[1], 'light')!) // skala YALNIZ light blokta
|
|
171
|
+
const semantik = sinif.match(/^bg-([a-z-]+)$/)
|
|
172
|
+
if (semantik) return semantikRgb(semantik[1], blok)
|
|
173
|
+
throw new Error(`çözülemeyen sınıf: ${sinif}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** checked track'in ilgili temadaki rengi — `dark:` önekli sınıf dark'ta kazanır */
|
|
177
|
+
function checkedTrackRengi(varyant: string, blok: Blok): RGB {
|
|
178
|
+
const KOSUL = 'data-[state=checked]:'
|
|
179
|
+
const adaylar = varyantSinifi(varyant)
|
|
180
|
+
.split(/\s+/)
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.filter((s) => s.replace(/^dark:/, '').startsWith(KOSUL))
|
|
183
|
+
const soy = (s: string) => s.replace(/^dark:/, '').slice(KOSUL.length)
|
|
184
|
+
const darkSinif = adaylar.find((s) => s.startsWith('dark:'))
|
|
185
|
+
const lightSinif = adaylar.find((s) => !s.startsWith('dark:'))
|
|
186
|
+
if (!lightSinif) throw new Error(`${varyant}: temel (light) checked sınıfı yok`)
|
|
187
|
+
return sinifRengi(soy(blok === 'dark' && darkSinif ? darkSinif : lightSinif), blok)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Thumb'ın dolgusu — A ve B eksenlerinin AYNI sayıyı vermesinin sebebi */
|
|
191
|
+
function thumbRengi(blok: Blok): RGB {
|
|
192
|
+
const m = THUMB_CVA.match(/"([^"]*\bbg-[a-z-]+\b[^"]*)"/)
|
|
193
|
+
if (!m) throw new Error('thumb taban sınıfı okunamadı')
|
|
194
|
+
const bg = m[1].split(/\s+/).find((s) => s.startsWith('bg-'))!
|
|
195
|
+
return sinifRengi(bg, blok)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const KADEMELER = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
199
|
+
const ESIK = 3.0
|
|
200
|
+
const DELTAE_ESIK = 10
|
|
201
|
+
|
|
202
|
+
/** Bir rengin switch eksenlerinde (A + B) o temada eşiği geçip geçmediği */
|
|
203
|
+
const eksenleriGecer = (c: RGB, blok: Blok): boolean =>
|
|
204
|
+
zeminler(blok).every(([, z]) => kontrast(c, z) >= ESIK) &&
|
|
205
|
+
kontrast(thumbRengi(blok), c) >= ESIK
|
|
206
|
+
|
|
207
|
+
describe('#454 — hesaplayıcı çapaları', () => {
|
|
208
|
+
it('kontrast hesaplayıcısı DOĞRU — belgelenmiş token çifti beklenen oranı veriyor', () => {
|
|
209
|
+
// tokens.css `--info-subtle` / `--info-subtle-foreground` için 9.5:1 iddia ediyor
|
|
210
|
+
const oran = kontrast(
|
|
211
|
+
semantikRgb('info-subtle', 'light'),
|
|
212
|
+
semantikRgb('info-subtle-foreground', 'light')
|
|
213
|
+
)
|
|
214
|
+
expect(oran).toBeGreaterThan(9.3)
|
|
215
|
+
expect(oran).toBeLessThan(9.7)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('ΔE hesaplayıcısı DOĞRU — aynı renk 0, siyah↔beyaz 100', () => {
|
|
219
|
+
expect(deltaE([100, 116, 139], [100, 116, 139])).toBeCloseTo(0, 6)
|
|
220
|
+
expect(deltaE([0, 0, 0], [255, 255, 255])).toBeCloseTo(100, 0)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('hesaplayıcı, issue #454 gövdesindeki ESKİ ölçümleri birebir üretiyor', () => {
|
|
224
|
+
// 1.154 / 1.359 / 1.197 — `bg-accent` ile checked track ↔ üç zemin
|
|
225
|
+
const eski = (blok: Blok) =>
|
|
226
|
+
zeminler(blok).map(([, z]) =>
|
|
227
|
+
Number(kontrast(semantikRgb('accent', blok), z).toFixed(3))
|
|
228
|
+
)
|
|
229
|
+
expect(eski('light')[0]).toBe(1.154)
|
|
230
|
+
expect(eski('dark')[0]).toBe(1.359)
|
|
231
|
+
expect(eski('dark')[2]).toBe(1.197)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('#454 — iki eksenin AYNI olmasının yapısal sebebi', () => {
|
|
236
|
+
it('thumb dolgusu `bg-background` — B ekseni A ekseniyle aynı renk çiftini ölçer', () => {
|
|
237
|
+
// Biri thumb'a ayrı bir dolgu verirse iki eksen ayrışır ve bu dosyanın
|
|
238
|
+
// "tek ölçüm iki ekseni kapatır" varsayımı sessizce geçersizleşir.
|
|
239
|
+
expect(THUMB_CVA).toContain('bg-background')
|
|
240
|
+
for (const blok of ['light', 'dark'] as const) {
|
|
241
|
+
expect(thumbRengi(blok)).toEqual(semantikRgb('background', blok))
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('durum ayrıca KONUMLA da taşınıyor — thumb ötelemeleri korunuyor', () => {
|
|
246
|
+
// Renk tek başına durum taşıyıcısı değil; `translate-x` ayrımı kapsam kilidi.
|
|
247
|
+
expect(THUMB_CVA).toContain('data-[state=checked]:translate-x-4')
|
|
248
|
+
expect(THUMB_CVA).toContain('data-[state=checked]:translate-x-5')
|
|
249
|
+
expect(THUMB_CVA).toContain('data-[state=checked]:translate-x-7')
|
|
250
|
+
expect(THUMB_CVA).toContain('data-[state=unchecked]:translate-x-0')
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('#454 — KABUL KRİTERİ: her varyantın checked track’i iki eksende ≥3:1', () => {
|
|
255
|
+
for (const varyant of VARYANTLAR) {
|
|
256
|
+
for (const blok of ['light', 'dark'] as const) {
|
|
257
|
+
it(`${varyant} / ${blok}`, () => {
|
|
258
|
+
const c = checkedTrackRengi(varyant, blok)
|
|
259
|
+
for (const [ad, z] of zeminler(blok)) {
|
|
260
|
+
// Zemin adı hata mesajına girsin diye ayrı assertion
|
|
261
|
+
expect(`${ad}: ${kontrast(c, z) >= ESIK ? 'geçer' : kontrast(c, z).toFixed(3)}`).toBe(
|
|
262
|
+
`${ad}: geçer`
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
expect(kontrast(thumbRengi(blok), c)).toBeGreaterThanOrEqual(ESIK)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('#454 — klon filtresi: varyantlar birbirinden ayırt edilebilir', () => {
|
|
272
|
+
for (const blok of ['light', 'dark'] as const) {
|
|
273
|
+
it(`${blok} — tüm varyant çiftleri ΔE*ab ≥ ${DELTAE_ESIK}`, () => {
|
|
274
|
+
for (let i = 0; i < VARYANTLAR.length; i++) {
|
|
275
|
+
for (let j = i + 1; j < VARYANTLAR.length; j++) {
|
|
276
|
+
const cift = `${VARYANTLAR[i]}↔${VARYANTLAR[j]}`
|
|
277
|
+
const d = deltaE(
|
|
278
|
+
checkedTrackRengi(VARYANTLAR[i], blok),
|
|
279
|
+
checkedTrackRengi(VARYANTLAR[j], blok)
|
|
280
|
+
)
|
|
281
|
+
// Çift adı hata mesajına girsin diye dizge karşılaştırması
|
|
282
|
+
expect(`${cift}: ${d >= DELTAE_ESIK ? 'ayrık' : d.toFixed(1)}`).toBe(`${cift}: ayrık`)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it(`${blok} — checked track, UNCHECKED track’ten (bg-input) ayırt edilebilir`, () => {
|
|
288
|
+
// Açık switch'in kapalıya benzemesi tam olarak bu issue'nun şikâyeti.
|
|
289
|
+
const unchecked = semantikRgb('input', blok)
|
|
290
|
+
for (const varyant of VARYANTLAR) {
|
|
291
|
+
expect(deltaE(checkedTrackRengi(varyant, blok), unchecked)).toBeGreaterThanOrEqual(
|
|
292
|
+
DELTAE_ESIK
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('#454 — `secondary` kararı: ad ↔ token tutarsızlığı ve reddedilen alternatifler', () => {
|
|
300
|
+
it('ESKİ hâli (`bg-accent`) üç zeminde de eşiğin ALTINDA — kusur gerçek', () => {
|
|
301
|
+
for (const blok of ['light', 'dark'] as const) {
|
|
302
|
+
for (const [, z] of zeminler(blok)) {
|
|
303
|
+
expect(kontrast(semantikRgb('accent', blok), z)).toBeLessThan(ESIK)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('SEMANTİK `bg-secondary` ELENDİ — light’ta `--accent` ile BİREBİR aynı, dark’ta daha kötü', () => {
|
|
309
|
+
// "Adı secondary ise bg-secondary kullansın" en bariz alternatif; ölçümle elendi.
|
|
310
|
+
expect(tokenOku('secondary', 'light')).toBe(tokenOku('accent', 'light'))
|
|
311
|
+
expect(kontrast(semantikRgb('secondary', 'light'), semantikRgb('background', 'light'))).toBeLessThan(ESIK)
|
|
312
|
+
expect(kontrast(semantikRgb('secondary', 'dark'), semantikRgb('background', 'dark'))).toBeLessThan(1.05)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('`bg-secondary-foreground` ELENDİ — dark’ta `--primary` ile BİREBİR aynı (primary klonu)', () => {
|
|
316
|
+
expect(tokenOku('secondary-foreground', 'dark')).toBe(tokenOku('primary', 'dark'))
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('slate skalasında iki temayı birden geçen VE klon olmayan TEK kademe 500', () => {
|
|
320
|
+
const uygun = KADEMELER.filter((k) => {
|
|
321
|
+
const ham = tokenOku(`secondary-${k}`, 'light')
|
|
322
|
+
if (!ham) return false
|
|
323
|
+
const c = rgbTokenRgb(ham)
|
|
324
|
+
return (['light', 'dark'] as const).every(
|
|
325
|
+
(blok) =>
|
|
326
|
+
eksenleriGecer(c, blok) &&
|
|
327
|
+
(['primary', 'success', 'warning', 'destructive'] as Varyant[]).every(
|
|
328
|
+
(v) => deltaE(c, checkedTrackRengi(v, blok)) >= DELTAE_ESIK
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
expect(uygun).toEqual([500])
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('#448’in slider’daki 500-elemesi BURADA GEÇERSİZ — switch’in `accent` varyantı YOK', () => {
|
|
336
|
+
// #453 ölçümü: `--secondary-500` ↔ slider'ın `secondary` varyantı ΔE = 0.0 (klon).
|
|
337
|
+
// Switch'te o çakışma yok; kaynak varyant listesi bunu kanıtlar.
|
|
338
|
+
expect(VARYANTLAR).not.toContain('accent')
|
|
339
|
+
expect(TRACK_CVA).not.toMatch(/\n\s*accent:/)
|
|
340
|
+
const k500 = rgbTokenRgb(tokenOku('secondary-500', 'light')!)
|
|
341
|
+
for (const blok of ['light', 'dark'] as const) {
|
|
342
|
+
for (const v of ['primary', 'success', 'warning', 'destructive'] as Varyant[]) {
|
|
343
|
+
expect(deltaE(k500, checkedTrackRengi(v, blok))).toBeGreaterThanOrEqual(DELTAE_ESIK)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('`--accent-50 … --accent-950` skalası tokens.css’te TANIMLI DEĞİL', () => {
|
|
349
|
+
// Gerçek bir accent skalası eklenirse "varyant kendi ailesine dönsün mü?"
|
|
350
|
+
// sorusu yeniden açılır — burası o an kırmızıya döner.
|
|
351
|
+
for (const k of KADEMELER) expect(tokenOku(`accent-${k}`, 'light')).toBeNull()
|
|
352
|
+
expect(tokenOku('secondary-500', 'light')).not.toBeNull()
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('#454 — `warning` kardeş kusuru: kök neden ve çözüm şekli', () => {
|
|
357
|
+
it('KÖK NEDEN: `--warning` `.dark` bloğunda EZİLMİYOR (kardeşleri tema-uyarlanabilir)', () => {
|
|
358
|
+
expect(tokenOku('warning', 'dark')).toBeNull()
|
|
359
|
+
expect(tokenOku('success', 'dark')).not.toBeNull() // karşıtlık
|
|
360
|
+
expect(tokenOku('accent', 'dark')).not.toBeNull() // karşıtlık (#448 ile farkı)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('ESKİ hâli (`bg-warning`) LIGHT’ta eşiğin altında, DARK’ta değil', () => {
|
|
364
|
+
expect(kontrast(semantikRgb('warning', 'light'), semantikRgb('background', 'light'))).toBeLessThan(ESIK)
|
|
365
|
+
expect(kontrast(semantikRgb('warning', 'dark'), semantikRgb('background', 'dark'))).toBeGreaterThan(ESIK)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('tema-KÖR bir warning kademesi seçilebilirdi ama DARK’ı gereksizce oynatırdı', () => {
|
|
369
|
+
// `dark:` ayrımının gerekçesi: dark zaten geçiyordu, regresyon yüzeyi sıfır tutuldu.
|
|
370
|
+
const temaKor = KADEMELER.filter((k) => {
|
|
371
|
+
const ham = tokenOku(`warning-${k}`, 'light')
|
|
372
|
+
return ham && (['light', 'dark'] as const).every((b) => eksenleriGecer(rgbTokenRgb(ham), b))
|
|
373
|
+
})
|
|
374
|
+
expect(temaKor).toEqual([700])
|
|
375
|
+
expect(checkedTrackRengi('warning', 'dark')).toEqual(semantikRgb('warning', 'dark'))
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('LIGHT’ta eşiği geçen kademe kümesi {700,800,900,950} — en açığı seçildi', () => {
|
|
379
|
+
const gecen = KADEMELER.filter((k) => {
|
|
380
|
+
const ham = tokenOku(`warning-${k}`, 'light')
|
|
381
|
+
return ham && eksenleriGecer(rgbTokenRgb(ham), 'light')
|
|
382
|
+
})
|
|
383
|
+
expect(gecen).toEqual([700, 800, 900, 950])
|
|
384
|
+
expect(checkedTrackRengi('warning', 'light')).toEqual(
|
|
385
|
+
rgbTokenRgb(tokenOku('warning-700', 'light')!)
|
|
386
|
+
)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('DARK render BİT-BİT AYNI kaldı (warning + dokunulmayan varyantlar)', () => {
|
|
390
|
+
for (const v of ['primary', 'success', 'warning', 'destructive'] as Varyant[]) {
|
|
391
|
+
const tok = v === 'destructive' ? 'error' : v
|
|
392
|
+
expect(checkedTrackRengi(v, 'dark')).toEqual(semantikRgb(tok, 'dark'))
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
describe('#454 — kaynak sözleşmesi (switch.tsx)', () => {
|
|
398
|
+
it('`secondary` → slate skalasının 500 kademesi, `dark:` ayrımı YOK', () => {
|
|
399
|
+
expect(varyantSinifi('secondary')).toBe('data-[state=checked]:bg-[rgb(var(--secondary-500))]')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('`warning` → light -700, dark semantik token', () => {
|
|
403
|
+
expect(varyantSinifi('warning')).toBe(
|
|
404
|
+
'data-[state=checked]:bg-[rgb(var(--warning-700))] dark:data-[state=checked]:bg-warning'
|
|
405
|
+
)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('KAPSAM KİLİDİ: geçen üç varyant DOKUNULMADI', () => {
|
|
409
|
+
expect(varyantSinifi('primary')).toBe('data-[state=checked]:bg-primary')
|
|
410
|
+
expect(varyantSinifi('success')).toBe('data-[state=checked]:bg-success')
|
|
411
|
+
expect(varyantSinifi('destructive')).toBe('data-[state=checked]:bg-error')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('KAPSAM KİLİDİ: unchecked track ve prop yüzeyi DEĞİŞMEDİ', () => {
|
|
415
|
+
expect(TRACK_CVA).toContain('data-[state=unchecked]:bg-input')
|
|
416
|
+
expect(kaynak).toContain(
|
|
417
|
+
'type SwitchVariant = "primary" | "success" | "warning" | "destructive" | "secondary";'
|
|
418
|
+
)
|
|
419
|
+
expect(kaynak).toContain('export { Switch, switchVariants }')
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('`bg-secondary-500` / `bg-warning-700` YAZILMADI — preset o sınıfları üretmiyor (#307 ölü-sınıf tuzağı)', () => {
|
|
423
|
+
const preset = fs.readFileSync(path.resolve(KOK, '../tailwind-preset.js'), 'utf8')
|
|
424
|
+
expect(preset).not.toMatch(/"?secondary"?:\s*\{[^}]*\b500:/s)
|
|
425
|
+
expect(preset).not.toMatch(/\bwarning:\s*\{[^}]*\b700:/s)
|
|
426
|
+
expect(TRACK_CVA).not.toContain('bg-secondary-500')
|
|
427
|
+
expect(TRACK_CVA).not.toContain('bg-warning-700')
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
describe('#454 — render sözleşmesi (<Switch />)', () => {
|
|
432
|
+
const sinif = (jsx: React.ReactElement) => {
|
|
433
|
+
const { container } = render(jsx)
|
|
434
|
+
return (container.querySelector('[role="switch"]') as HTMLElement).className
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
it('checked `secondary` switch slate-500 sınıfını taşıyor', () => {
|
|
438
|
+
expect(sinif(<Switch variant="secondary" defaultChecked />)).toContain(
|
|
439
|
+
'data-[state=checked]:bg-[rgb(var(--secondary-500))]'
|
|
440
|
+
)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('checked `secondary` switch ARTIK `bg-accent` taşımıyor', () => {
|
|
444
|
+
expect(sinif(<Switch variant="secondary" defaultChecked />)).not.toContain(
|
|
445
|
+
'data-[state=checked]:bg-accent'
|
|
446
|
+
)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('checked `warning` switch light -700 + dark semantik sınıflarını birlikte taşıyor', () => {
|
|
450
|
+
const c = sinif(<Switch variant="warning" defaultChecked />)
|
|
451
|
+
expect(c).toContain('data-[state=checked]:bg-[rgb(var(--warning-700))]')
|
|
452
|
+
expect(c).toContain('dark:data-[state=checked]:bg-warning')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('varsayılan varyant (primary) ve unchecked davranışı korunuyor', () => {
|
|
456
|
+
const c = sinif(<Switch />)
|
|
457
|
+
expect(c).toContain('data-[state=checked]:bg-primary')
|
|
458
|
+
expect(c).toContain('data-[state=unchecked]:bg-input')
|
|
459
|
+
})
|
|
460
|
+
})
|
|
@@ -77,10 +77,14 @@ describe('Switch Component', () => {
|
|
|
77
77
|
expect(switchElement).toHaveClass('data-[state=checked]:bg-success')
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
// #454: `--warning` `.dark` bloğunda ezilmediği için light temada checked
|
|
81
|
+
// track ↔ zemin 2.133 idi (eşik 3:1). Light'ta skalanın -700 kademesine
|
|
82
|
+
// inildi, dark semantik token'da kaldı. Ölçüm: switch-variant-contrast.test.tsx
|
|
80
83
|
it('renders warning variant correctly', () => {
|
|
81
84
|
render(<Switch variant="warning" data-testid="switch" />)
|
|
82
85
|
const switchElement = screen.getByTestId('switch')
|
|
83
|
-
expect(switchElement).toHaveClass('data-[state=checked]:bg-warning')
|
|
86
|
+
expect(switchElement).toHaveClass('data-[state=checked]:bg-[rgb(var(--warning-700))]')
|
|
87
|
+
expect(switchElement).toHaveClass('dark:data-[state=checked]:bg-warning')
|
|
84
88
|
})
|
|
85
89
|
|
|
86
90
|
it('renders destructive variant correctly', () => {
|
|
@@ -89,10 +93,15 @@ describe('Switch Component', () => {
|
|
|
89
93
|
expect(switchElement).toHaveClass('data-[state=checked]:bg-error')
|
|
90
94
|
})
|
|
91
95
|
|
|
96
|
+
// #454: varyantın ADI `secondary` iken TOKEN'ı `accent` idi; `--accent` bir
|
|
97
|
+
// yüzey token'ı olduğu için checked switch iki temada da görünmüyordu
|
|
98
|
+
// (1.154 / 1.359). Varyant kendi ailesine, `--secondary-*` slate skalasına
|
|
99
|
+
// bağlandı. Ölçüm: switch-variant-contrast.test.tsx
|
|
92
100
|
it('renders secondary variant correctly', () => {
|
|
93
101
|
render(<Switch variant="secondary" data-testid="switch" />)
|
|
94
102
|
const switchElement = screen.getByTestId('switch')
|
|
95
|
-
expect(switchElement).toHaveClass('data-[state=checked]:bg-
|
|
103
|
+
expect(switchElement).toHaveClass('data-[state=checked]:bg-[rgb(var(--secondary-500))]')
|
|
104
|
+
expect(switchElement).not.toHaveClass('data-[state=checked]:bg-accent')
|
|
96
105
|
})
|
|
97
106
|
|
|
98
107
|
it('uses primary variant as default', () => {
|