@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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #442 — Slider `warning` / `error` kontrast değişmezleri (İKİ AYRI EKSEN).
|
|
3
|
+
*
|
|
4
|
+
* BULGU (#424/PR #440'ın progress'te kapattığı kusurun birebir slider ikizi):
|
|
5
|
+
*
|
|
6
|
+
* EKSEN A (range↔track): range `bg-warning`/`bg-error`, track `bg-warning/20`·
|
|
7
|
+
* `bg-error/20` — dolgu ile track AYNI token'ın iki alfa değeri. LIGHT temada
|
|
8
|
+
* ayrım kayboluyordu: warning 1.835 · error 2.893 (eşik 3:1, WCAG 1.4.11
|
|
9
|
+
* non-text). DARK zaten temizdi (6.97 / 4.46) → bulgu LIGHT'a özgü.
|
|
10
|
+
*
|
|
11
|
+
* EKSEN B (thumb kenarlığı↔zemin): tutamaç `border-warning`/`border-error` +
|
|
12
|
+
* `bg-background`. Dolgu zeminin kendisi olduğundan ayırt edilebilirliği YALNIZ
|
|
13
|
+
* kenarlık taşır. Ölçüm: warning light **2.133 ✗**, error light 3.781 ✓.
|
|
14
|
+
* Yani bu eksende yalnız `warning` kusurluydu; `error` tutarlılık için (aşağıda
|
|
15
|
+
* "tek renk" kilidi) aynı ifadeye çekildi ve kontrastı ARTTI (3.781 → 6.470).
|
|
16
|
+
*
|
|
17
|
+
* axe ikisini de yakalamaz: `color-contrast` yalnız METİN öğelerini ölçer.
|
|
18
|
+
*
|
|
19
|
+
* KÖK NEDEN — token seviyesinde eksik tema-uyarlaması:
|
|
20
|
+
* `--warning` ve `--error` tokens.css'te `.dark` bloğunda EZİLMİYOR; iki temada da
|
|
21
|
+
* aynı renk. Kardeşleri tema-uyarlanabilir olduğu için eşiği geçiyor (`--success`
|
|
22
|
+
* light %29 → dark %36). Bu iki aile o uyarlamadan yoksun.
|
|
23
|
+
*
|
|
24
|
+
* ÇÖZÜM: eksik uyarlamayı component düzeyinde ver — light'ta skalanın -700
|
|
25
|
+
* kademesi, dark'ta semantik token:
|
|
26
|
+
* range: "bg-[rgb(var(--warning-700))] dark:bg-warning"
|
|
27
|
+
* thumb: "border-[rgb(var(--warning-700))] dark:border-warning bg-background"
|
|
28
|
+
* Tema-KÖR -700 dark'ı kırardı (aşağıda kanıtlanıyor). #389'un tek-orta-ton
|
|
29
|
+
* (`--secondary-500`) çözümü burada UYGULANAMAZ: orada `--secondary` temaya göre
|
|
30
|
+
* değişiyordu. Track alfaları bu yüzden `/20` KALDI.
|
|
31
|
+
*
|
|
32
|
+
* NOT: #389/#407'nin `slider-secondary-contrast.test.ts` dosyasından BİLEREK ayrı —
|
|
33
|
+
* o dosya `--secondary` eksenini kilitler, bu dosya `--warning`/`--error` eksenini.
|
|
34
|
+
* Pro ikizi: packages/moonui-pro/.../slider-warning-error-contrast.test.tsx
|
|
35
|
+
*/
|
|
36
|
+
import * as React from 'react'
|
|
37
|
+
import { render } from '@testing-library/react'
|
|
38
|
+
import '@testing-library/jest-dom'
|
|
39
|
+
import * as fs from 'fs'
|
|
40
|
+
import * as path from 'path'
|
|
41
|
+
import { Slider } from '../slider'
|
|
42
|
+
|
|
43
|
+
const KOK = path.resolve(__dirname, '../../..')
|
|
44
|
+
const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
|
|
45
|
+
|
|
46
|
+
/** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
|
|
47
|
+
function tokenOku(ad: string, blok: 'light' | 'dark'): string | null {
|
|
48
|
+
const kaynak =
|
|
49
|
+
blok === 'dark'
|
|
50
|
+
? tokens.slice(tokens.indexOf('.dark'))
|
|
51
|
+
: tokens.slice(0, tokens.indexOf('.dark'))
|
|
52
|
+
return kaynak.match(new RegExp(`--${ad}:\\s*([^;]+);`))?.[1].trim() ?? null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
56
|
+
h /= 360
|
|
57
|
+
s /= 100
|
|
58
|
+
l /= 100
|
|
59
|
+
const f = (n: number) => {
|
|
60
|
+
const k = (n + h * 12) % 12
|
|
61
|
+
const a = s * Math.min(l, 1 - l)
|
|
62
|
+
return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
|
|
63
|
+
}
|
|
64
|
+
return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hslTokenRgb = (v: string): [number, number, number] => {
|
|
68
|
+
const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
|
|
69
|
+
return hslToRgb(h, s, l)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Skala token'ları ("--warning-700: 161 98 7") HSL DEĞİL, RGB üçlüsüdür (semantik
|
|
74
|
+
* token'ların HSL formatından farklı) — component'te bu yüzden
|
|
75
|
+
* `rgb(var(--warning-700))` kompozisyonu kullanılır, hsl() değil.
|
|
76
|
+
*/
|
|
77
|
+
const rgbTokenRgb = (v: string): [number, number, number] => {
|
|
78
|
+
const p = v.split(/\s+/).map((x) => parseFloat(x))
|
|
79
|
+
return [p[0], p[1], p[2]]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function bagilParlaklik([r, g, b]: [number, number, number]): number {
|
|
83
|
+
const c = (v: number) => {
|
|
84
|
+
const x = v / 255
|
|
85
|
+
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
|
|
86
|
+
}
|
|
87
|
+
return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rgbKontrast(a: [number, number, number], b: [number, number, number]): number {
|
|
91
|
+
const la = bagilParlaklik(a)
|
|
92
|
+
const lb = bagilParlaklik(b)
|
|
93
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
94
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** `bg-warning/20` gibi alfa katmanının zemin üstündeki bileşke rengi */
|
|
98
|
+
function alfaBileske(
|
|
99
|
+
ust: [number, number, number],
|
|
100
|
+
alfa: number,
|
|
101
|
+
zemin: [number, number, number]
|
|
102
|
+
): [number, number, number] {
|
|
103
|
+
return [
|
|
104
|
+
Math.round(alfa * ust[0] + (1 - alfa) * zemin[0]),
|
|
105
|
+
Math.round(alfa * ust[1] + (1 - alfa) * zemin[1]),
|
|
106
|
+
Math.round(alfa * ust[2] + (1 - alfa) * zemin[2]),
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Docs kabuğunda ölçülen GERÇEK koyu kart zemini (#111827 = rgb(17,24,39)) —
|
|
112
|
+
* #284/#368 konvansiyonu: kontrast, --background'ın yanında bu gerçek yüzeye göre
|
|
113
|
+
* de ölçülür. Bu bulguda DARBOĞAZ: en düşük dark oranlarını o veriyor.
|
|
114
|
+
*/
|
|
115
|
+
const KART_GERCEK_DARK: [number, number, number] = [17, 24, 39]
|
|
116
|
+
|
|
117
|
+
const AILELER = ['warning', 'error'] as const
|
|
118
|
+
type Aile = (typeof AILELER)[number]
|
|
119
|
+
|
|
120
|
+
/** Semantik token'ın ilgili temadaki değeri — `.dark` ezmesi YOKSA light'a düşer */
|
|
121
|
+
function semantikRgb(aile: Aile, blok: 'light' | 'dark'): [number, number, number] {
|
|
122
|
+
return hslTokenRgb(tokenOku(aile, blok) ?? tokenOku(aile, 'light')!)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** O temada slider'ın oturabileceği tüm zeminler */
|
|
126
|
+
function zeminler(blok: 'light' | 'dark'): Array<[string, [number, number, number]]> {
|
|
127
|
+
const z: Array<[string, [number, number, number]]> = [
|
|
128
|
+
['--background', hslTokenRgb(tokenOku('background', blok)!)],
|
|
129
|
+
['--card', hslTokenRgb(tokenOku('card', blok)!)],
|
|
130
|
+
]
|
|
131
|
+
if (blok === 'dark') z.push(['gerçek kart #111827', KART_GERCEK_DARK])
|
|
132
|
+
return z
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ——— Renkler KAYNAKTAN türetilir ———————————————————————————————————————
|
|
136
|
+
// PR #440'ın dersi: ölçümü `-700` VARSAYIMINA göre yaparsan, biri değeri eskiye
|
|
137
|
+
// döndürdüğünde kriter testleri yeşil kalır (boş-yeşil). Bu yüzden renkler
|
|
138
|
+
// slider.tsx'in O ANKİ CVA sınıf dizgelerinden çözülür.
|
|
139
|
+
const kaynak = fs.readFileSync(path.join(KOK, 'components/ui/slider.tsx'), 'utf8')
|
|
140
|
+
const iTrack = kaynak.indexOf('sliderTrackVariants')
|
|
141
|
+
const iRange = kaynak.indexOf('sliderRangeVariants')
|
|
142
|
+
const iThumb = kaynak.indexOf('sliderThumbVariants')
|
|
143
|
+
const iSon = kaynak.indexOf('SliderBaseProps')
|
|
144
|
+
const TRACK_BLOK = kaynak.slice(iTrack, iRange)
|
|
145
|
+
const RANGE_BLOK = kaynak.slice(iRange, iThumb)
|
|
146
|
+
const THUMB_BLOK = kaynak.slice(iThumb, iSon)
|
|
147
|
+
|
|
148
|
+
/** CVA bloğundan `<aile>: "<sınıflar>"` değerini oku */
|
|
149
|
+
function varyantSinifi(blok: string, aile: string): string {
|
|
150
|
+
const m = blok.match(new RegExp(`\\n\\s*${aile}:\\s*"([^"]+)"`))
|
|
151
|
+
if (!m) throw new Error(`${aile} varyantı bulunamadı`)
|
|
152
|
+
return m[1]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Tek bir Tailwind sınıfını renge çevir (arbitrary skala VEYA semantik token) */
|
|
156
|
+
function sinifRengi(sinif: string, blok: 'light' | 'dark'): [number, number, number] {
|
|
157
|
+
const skala = sinif.match(/^(?:bg|border)-\[rgb\(var\(--([a-z0-9-]+)\)\)\]$/)
|
|
158
|
+
if (skala) return rgbTokenRgb(tokenOku(skala[1], 'light')!) // skala yalnız :root'ta
|
|
159
|
+
const semantik = sinif.match(/^(?:bg|border)-([a-z-]+)$/)
|
|
160
|
+
if (semantik)
|
|
161
|
+
return hslTokenRgb(tokenOku(semantik[1], blok) ?? tokenOku(semantik[1], 'light')!)
|
|
162
|
+
throw new Error(`çözülemeyen sınıf: ${sinif}`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Varyant dizgesinden o temada GERÇEKTEN uygulanan rengi çöz.
|
|
167
|
+
* `dark:` önekli sınıf dark temada kazanır; `onek` ile eksen seçilir
|
|
168
|
+
* (thumb dizgesi `border-*` yanında `bg-background` da taşır).
|
|
169
|
+
*/
|
|
170
|
+
function uygulananRenk(
|
|
171
|
+
cvaBlok: string,
|
|
172
|
+
aile: Aile,
|
|
173
|
+
onek: 'bg' | 'border',
|
|
174
|
+
blok: 'light' | 'dark'
|
|
175
|
+
): [number, number, number] {
|
|
176
|
+
const siniflar = varyantSinifi(cvaBlok, aile)
|
|
177
|
+
.split(/\s+/)
|
|
178
|
+
.filter((s) => s.replace(/^dark:/, '').startsWith(`${onek}-`))
|
|
179
|
+
const darkSinif = siniflar.find((s) => s.startsWith('dark:'))?.slice('dark:'.length)
|
|
180
|
+
const lightSinif = siniflar.find((s) => !s.startsWith('dark:'))
|
|
181
|
+
if (!lightSinif) throw new Error(`${aile}/${onek}: temel (light) sınıf yok`)
|
|
182
|
+
return sinifRengi(blok === 'dark' ? darkSinif ?? lightSinif : lightSinif, blok)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const rangeRengi = (aile: Aile, blok: 'light' | 'dark') =>
|
|
186
|
+
uygulananRenk(RANGE_BLOK, aile, 'bg', blok)
|
|
187
|
+
const thumbKenarRengi = (aile: Aile, blok: 'light' | 'dark') =>
|
|
188
|
+
uygulananRenk(THUMB_BLOK, aile, 'border', blok)
|
|
189
|
+
|
|
190
|
+
/** Track'in her zemin üstündeki bileşkesi — alfa da KAYNAKTAN okunur */
|
|
191
|
+
function trackler(aile: Aile, blok: 'light' | 'dark') {
|
|
192
|
+
const sinif = varyantSinifi(TRACK_BLOK, aile)
|
|
193
|
+
const m = sinif.match(/^bg-([a-z-]+)\/(\d+)$/)
|
|
194
|
+
if (!m) throw new Error(`track sınıfı çözülemedi: ${sinif}`)
|
|
195
|
+
const taban = hslTokenRgb(tokenOku(m[1], blok) ?? tokenOku(m[1], 'light')!)
|
|
196
|
+
const alfa = Number(m[2]) / 100
|
|
197
|
+
return zeminler(blok).map(([ad, z]) => [ad, alfaBileske(taban, alfa, z)] as const)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const KADEMELER = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
201
|
+
|
|
202
|
+
/** Slider DOM'u: kök > .moonui-theme > track > range; thumb'lar track'in KARDEŞİ */
|
|
203
|
+
function parcalar(container: HTMLElement) {
|
|
204
|
+
const kabuk = container.querySelector('.moonui-theme') as HTMLElement
|
|
205
|
+
expect(kabuk).not.toBeNull()
|
|
206
|
+
const track = kabuk.firstElementChild as HTMLElement
|
|
207
|
+
const range = track.firstElementChild as HTMLElement
|
|
208
|
+
const thumb = kabuk.querySelector('[role="slider"]') as HTMLElement
|
|
209
|
+
expect(range).not.toBeNull()
|
|
210
|
+
expect(thumb).not.toBeNull()
|
|
211
|
+
return { track, range, thumb }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
describe('#442 slider warning/error — hesaplayıcı çapası', () => {
|
|
215
|
+
it('hesaplayıcı DOĞRU — belgelenmiş bir token çifti beklenen oranı veriyor', () => {
|
|
216
|
+
// Sessiz-yeşil koruması: tokens.css `--info-subtle` çiftini 9.52:1 diye
|
|
217
|
+
// belgeliyor; hesaplayıcı bozulursa bu çapa kırmızı olur.
|
|
218
|
+
const oran = rgbKontrast(
|
|
219
|
+
hslTokenRgb(tokenOku('info-subtle', 'light')!),
|
|
220
|
+
hslTokenRgb(tokenOku('info-subtle-foreground', 'light')!)
|
|
221
|
+
)
|
|
222
|
+
expect(oran).toBeGreaterThan(9.3)
|
|
223
|
+
expect(oran).toBeLessThan(9.7)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('hesaplayıcı, denetimin TARAYICIDA ölçtüğü renkleri birebir üretiyor', () => {
|
|
227
|
+
// Çapraz doğrulama (#424/#442 gövdesindeki gerçek tarayıcı ölçümleri): model
|
|
228
|
+
// bunları tutturamıyorsa aşağıdaki tüm oranlar anlamsızdır.
|
|
229
|
+
expect(semantikRgb('warning', 'light')).toEqual([245, 159, 10])
|
|
230
|
+
expect(semantikRgb('error', 'light')).toEqual([239, 67, 67])
|
|
231
|
+
expect(trackler('warning', 'light')[0][1]).toEqual([253, 236, 206])
|
|
232
|
+
expect(trackler('error', 'light')[0][1]).toEqual([252, 217, 217])
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('#442 kök neden: --warning ve --error `.dark` bloğunda EZİLMİYOR', () => {
|
|
237
|
+
it.each(AILELER)('--%s dark ezmesi yok → iki temada aynı renk', (aile) => {
|
|
238
|
+
// Bulgunun yapısal kök nedeni. Biri ileride `.dark` bloğuna `--warning`/
|
|
239
|
+
// `--error` eklerse burası kırmızı olur ve component düzeyindeki `dark:`
|
|
240
|
+
// telafisinin yeniden ölçülmesi gerektiğini söyler.
|
|
241
|
+
expect(tokenOku(aile, 'dark')).toBeNull()
|
|
242
|
+
expect(semantikRgb(aile, 'light')).toEqual(semantikRgb(aile, 'dark'))
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('karşıtlık: --success tema-uyarlanabilir olduğu İÇİN eşiği geçiyor', () => {
|
|
246
|
+
// Neden success'e dokunulmadığının kanıtı: onun dark ezmesi VAR.
|
|
247
|
+
expect(tokenOku('success', 'dark')).not.toBeNull()
|
|
248
|
+
expect(tokenOku('success', 'light')).not.toBe(tokenOku('success', 'dark'))
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it.each(AILELER)('--%s-700 tanımlı ve RGB üçlüsü olarak ayrıştırılabiliyor', (aile) => {
|
|
252
|
+
const ham = tokenOku(`${aile}-700`, 'light')
|
|
253
|
+
expect(ham).toBeTruthy()
|
|
254
|
+
rgbTokenRgb(ham!).forEach((kanal) => {
|
|
255
|
+
expect(Number.isFinite(kanal)).toBe(true)
|
|
256
|
+
expect(kanal).toBeGreaterThanOrEqual(0)
|
|
257
|
+
expect(kanal).toBeLessThanOrEqual(255)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('zemin denkliği: `.moonui-theme` paket CSS’inde token yeniden tanımlamıyor', () => {
|
|
262
|
+
// Hesap, track'in/tutamacın altındaki yüzeyin --background/--card olduğunu
|
|
263
|
+
// VARSAYAR. Biri `.moonui-theme`i token kapsamına çevirirse o varsayım
|
|
264
|
+
// sessizce yanlışlanır — o an burası kırmızı olur (#389 deseni).
|
|
265
|
+
const stilDizini = path.join(KOK, 'styles')
|
|
266
|
+
const stiller = fs
|
|
267
|
+
.readdirSync(stilDizini)
|
|
268
|
+
.filter((f) => f.endsWith('.css'))
|
|
269
|
+
.map((f) => fs.readFileSync(path.join(stilDizini, f), 'utf8'))
|
|
270
|
+
.join('\n')
|
|
271
|
+
expect(stiller).not.toMatch(/\.moonui-theme[^{]*\{[^}]*--(warning|error|background|card)/)
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
describe('#442 kaynak sözleşmesi — slider.tsx', () => {
|
|
276
|
+
it.each(AILELER)('track %s sınıfı `/20` alfasını KORUYOR (yedi varyantın ortak deseni)', (aile) => {
|
|
277
|
+
// Alternatif öneri track'i `/10`'a çekmekti; kabul edilmedi (track↔zemin
|
|
278
|
+
// ayrımını zayıflatır, ortak konvansiyonu bozar). Burası o kararı kilitler.
|
|
279
|
+
expect(varyantSinifi(TRACK_BLOK, aile)).toBe(`bg-${aile}/20`)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it.each(AILELER)('EKSEN A: range %s light\'ta -700, dark\'ta semantik token', (aile) => {
|
|
283
|
+
expect(varyantSinifi(RANGE_BLOK, aile)).toBe(
|
|
284
|
+
`bg-[rgb(var(--${aile}-700))] dark:bg-${aile}`
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it.each(AILELER)('EKSEN B: thumb %s kenarlığı light\'ta -700, dark\'ta semantik token', (aile) => {
|
|
289
|
+
expect(varyantSinifi(THUMB_BLOK, aile)).toBe(
|
|
290
|
+
`border-[rgb(var(--${aile}-700))] dark:border-${aile} bg-background`
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('KAPSAM KİLİDİ: success ve primary DOKUNULMADI (ikisi de eşiği geçiyor)', () => {
|
|
295
|
+
// Gereksiz değişiklik yeni regresyon riski. Ölçüm: success A light 3.73 /
|
|
296
|
+
// dark 4.83 · primary A light 3.71 / dark 11.24 — ikisi de ≥3.
|
|
297
|
+
for (const aile of ['primary', 'success'] as const) {
|
|
298
|
+
expect(varyantSinifi(RANGE_BLOK, aile)).toBe(`bg-${aile}`)
|
|
299
|
+
expect(varyantSinifi(THUMB_BLOK, aile)).toBe(`border-${aile} bg-background`)
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
describe('#442 render sözleşmesi — <Slider />', () => {
|
|
305
|
+
it.each(AILELER)('%s: track eski token\'da, range yeni ifadeyle DOM\'a iniyor', (aile) => {
|
|
306
|
+
const { container } = render(
|
|
307
|
+
React.createElement(Slider, { trackVariant: aile, rangeVariant: aile })
|
|
308
|
+
)
|
|
309
|
+
const { track, range } = parcalar(container)
|
|
310
|
+
expect(track).toHaveClass(`bg-${aile}/20`)
|
|
311
|
+
expect(range.className).toContain(`bg-[rgb(var(--${aile}-700))]`)
|
|
312
|
+
expect(range.className).toContain(`dark:bg-${aile}`)
|
|
313
|
+
// Light'ta semantik token dolguya UYGULANMAMALI (track'in aynısıydı).
|
|
314
|
+
expect(range.className).not.toMatch(new RegExp(`(?:^|\\s)bg-${aile}(?:\\s|$)`))
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it.each(AILELER)('%s: thumb kenarlığı yeni ifadeyle iniyor, dolgusu bg-background kalıyor', (aile) => {
|
|
318
|
+
const { container } = render(React.createElement(Slider, { thumbVariant: aile }))
|
|
319
|
+
const { thumb } = parcalar(container)
|
|
320
|
+
expect(thumb.className).toContain(`border-[rgb(var(--${aile}-700))]`)
|
|
321
|
+
expect(thumb.className).toContain(`dark:border-${aile}`)
|
|
322
|
+
expect(thumb.className).toContain('bg-background')
|
|
323
|
+
expect(thumb.className).not.toMatch(new RegExp(`(?:^|\\s)border-${aile}(?:\\s|$)`))
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it.each(AILELER)('%s: thumbVariant düşüşü KORUNUYOR — tek prop track+range+thumb', (aile) => {
|
|
327
|
+
// `trackVariant || thumbVariant || "default"` ve `rangeVariant || thumbVariant ||
|
|
328
|
+
// "primary"` düşüşleri değişmedi; en yaygın kullanım bu yoldan geçer.
|
|
329
|
+
const { container } = render(React.createElement(Slider, { thumbVariant: aile }))
|
|
330
|
+
const { track, range, thumb } = parcalar(container)
|
|
331
|
+
expect(track).toHaveClass(`bg-${aile}/20`)
|
|
332
|
+
expect(range.className).toContain(`bg-[rgb(var(--${aile}-700))]`)
|
|
333
|
+
expect(thumb.className).toContain(`border-[rgb(var(--${aile}-700))]`)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('rangeVariant açık verildiğinde thumbVariant’ı hâlâ eziyor', () => {
|
|
337
|
+
const { container } = render(
|
|
338
|
+
React.createElement(Slider, { thumbVariant: 'warning', rangeVariant: 'success' })
|
|
339
|
+
)
|
|
340
|
+
const { range, thumb } = parcalar(container)
|
|
341
|
+
expect(range.className).toContain('bg-success')
|
|
342
|
+
expect(range.className).not.toContain('--warning-700')
|
|
343
|
+
// Eksenler bağımsız: tutamaç kendi varyantını korur.
|
|
344
|
+
expect(thumb.className).toContain('border-[rgb(var(--warning-700))]')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('diğer varyantlar ETKİLENMEDİ — primary range/thumb hâlâ token’larında', () => {
|
|
348
|
+
const { container } = render(
|
|
349
|
+
React.createElement(Slider, { rangeVariant: 'primary', thumbVariant: 'primary' })
|
|
350
|
+
)
|
|
351
|
+
const { range, thumb } = parcalar(container)
|
|
352
|
+
expect(range.className).toContain('bg-primary')
|
|
353
|
+
expect(thumb.className).toContain('border-primary')
|
|
354
|
+
expect(range.className).not.toContain('700')
|
|
355
|
+
expect(thumb.className).not.toContain('700')
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
describe.each(AILELER)('#442 %s — kabul kriteri ≥3:1 (WCAG 1.4.11)', (aile) => {
|
|
360
|
+
it.each(['light', 'dark'] as const)(
|
|
361
|
+
'EKSEN A (range↔track): %s temada TÜM zeminlerde geçiyor',
|
|
362
|
+
(blok) => {
|
|
363
|
+
// Ölçülen: warning light 4.235 · dark 6.97 (docs kartı 5.77)
|
|
364
|
+
// error light 4.950 · dark 4.46 (docs kartı 3.82)
|
|
365
|
+
const dolgu = rangeRengi(aile, blok)
|
|
366
|
+
for (const [ad, track] of trackler(aile, blok)) {
|
|
367
|
+
const oran = rgbKontrast(dolgu, track)
|
|
368
|
+
expect({ eksen: 'A', zemin: ad, gecti: oran >= 3.0 }).toEqual({
|
|
369
|
+
eksen: 'A',
|
|
370
|
+
zemin: ad,
|
|
371
|
+
gecti: true,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
it.each(['light', 'dark'] as const)(
|
|
378
|
+
'EKSEN B (thumb kenarlığı↔zemin): %s temada TÜM zeminlerde geçiyor',
|
|
379
|
+
(blok) => {
|
|
380
|
+
// Ölçülen: warning light 4.923 · dark 9.45 (docs kartı 8.32)
|
|
381
|
+
// error light 6.470 · dark 5.33 (docs kartı 4.69)
|
|
382
|
+
const kenar = thumbKenarRengi(aile, blok)
|
|
383
|
+
for (const [ad, zemin] of zeminler(blok)) {
|
|
384
|
+
const oran = rgbKontrast(kenar, zemin)
|
|
385
|
+
expect({ eksen: 'B', zemin: ad, gecti: oran >= 3.0 }).toEqual({
|
|
386
|
+
eksen: 'B',
|
|
387
|
+
zemin: ad,
|
|
388
|
+
gecti: true,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
it('ESKİ range (semantik token) LIGHT temada başarısızdı — bug’ın belgesi', () => {
|
|
395
|
+
// warning 1.835 · error 2.893. Düzeltmenin GEREKLİLİĞİNİ sabitler.
|
|
396
|
+
const eski = semantikRgb(aile, 'light')
|
|
397
|
+
for (const [, track] of trackler(aile, 'light')) {
|
|
398
|
+
expect(rgbKontrast(eski, track)).toBeLessThan(3.0)
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('DARK render DEĞİŞMEDİ — iki eksende de eski semantik token uygulanıyor', () => {
|
|
403
|
+
// Bulgu light'a özgüydü; düzeltmenin dark'ta hiçbir şeyi oynatmaması bilinçli
|
|
404
|
+
// bir kısıt (regresyon yüzeyini sıfıra indirir).
|
|
405
|
+
expect(rangeRengi(aile, 'dark')).toEqual(semantikRgb(aile, 'dark'))
|
|
406
|
+
expect(thumbKenarRengi(aile, 'dark')).toEqual(semantikRgb(aile, 'dark'))
|
|
407
|
+
for (const [, track] of trackler(aile, 'dark')) {
|
|
408
|
+
expect(rgbKontrast(semantikRgb(aile, 'dark'), track)).toBeGreaterThanOrEqual(3.0)
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('TEK RENK: light\'ta tutamaç kenarlığı ile range dolgusu AYNI renk', () => {
|
|
413
|
+
// #407'nin gerekçesi ("tutamaç ve dolgu tek renkte okunur") burada da geçerli
|
|
414
|
+
// ve `error` tutamacının NEDEN değiştirildiğinin kilidi: kontrastı zaten
|
|
415
|
+
// geçiyordu (3.781), ama range light'ta -700'e inince kenarlığı `border-error`
|
|
416
|
+
// bırakmak iki-tonlu bir görünüm üretirdi.
|
|
417
|
+
expect(thumbKenarRengi(aile, 'light')).toEqual(rangeRengi(aile, 'light'))
|
|
418
|
+
expect(thumbKenarRengi(aile, 'dark')).toEqual(rangeRengi(aile, 'dark'))
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
describe('#442 eksen B — ailelerin başlangıç durumu FARKLIYDI (kapsam kararının belgesi)', () => {
|
|
423
|
+
it('ESKİ thumb kenarlığı: warning LIGHT\'ta eşiğin ALTINDAYDI (2.133)', () => {
|
|
424
|
+
const eski = semantikRgb('warning', 'light')
|
|
425
|
+
for (const [, zemin] of zeminler('light')) {
|
|
426
|
+
expect(rgbKontrast(eski, zemin)).toBeLessThan(3.0)
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('ESKİ thumb kenarlığı: error LIGHT\'ta zaten geçiyordu (3.781) — değişim gerekçesi tutarlılık', () => {
|
|
431
|
+
// Dürüst kayıt: `error` tutamacı bir kontrast BUG'ı değildi. Aynı ifadeye
|
|
432
|
+
// çekilmesinin sebebi tek-renk tutarlılığı; ölçüm de düşmedi, yükseldi.
|
|
433
|
+
const eski = semantikRgb('error', 'light')
|
|
434
|
+
for (const [, zemin] of zeminler('light')) {
|
|
435
|
+
expect(rgbKontrast(eski, zemin)).toBeGreaterThanOrEqual(3.0)
|
|
436
|
+
}
|
|
437
|
+
const yeni = thumbKenarRengi('error', 'light')
|
|
438
|
+
expect(rgbKontrast(yeni, zeminler('light')[0][1])).toBeGreaterThan(
|
|
439
|
+
rgbKontrast(eski, zeminler('light')[0][1])
|
|
440
|
+
)
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
describe('#442 seçim gerekçesi — reddedilen alternatifler', () => {
|
|
445
|
+
it.each(AILELER)('TEMA-KÖR `-700` ataması %s varyantını DARK\'ta kırardı', (aile) => {
|
|
446
|
+
// Issue gövdesinin uyardığı refleks. -700 kademesi dark track'e karşı gerçek
|
|
447
|
+
// docs kartı zemininde eşiğin ALTINDA: warning 2.499 · error 2.232.
|
|
448
|
+
const k700 = rgbTokenRgb(tokenOku(`${aile}-700`, 'light')!)
|
|
449
|
+
const docsTrack = trackler(aile, 'dark').find(([ad]) => ad === 'gerçek kart #111827')![1]
|
|
450
|
+
expect(rgbKontrast(k700, docsTrack)).toBeLessThan(3.0)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it.each(AILELER)('#389\'un tek-orta-ton çözümü %s için UYGULANAMAZ — geçen kademe YOK', (aile) => {
|
|
454
|
+
// #389 dolguyu tek bir skala kademesine (`--secondary-500`) sabitleyip iki
|
|
455
|
+
// temayı birden geçebilmişti, çünkü `--secondary` temaya göre DEĞİŞİYOR.
|
|
456
|
+
// Burada semantik token sabit olduğundan track'ler birbirinden çok uzak:
|
|
457
|
+
// skalanın 11 kademesinin HİÇBİRİ tema-kör olarak iki temayı birden geçemiyor.
|
|
458
|
+
// Bu, `dark:` ayrımını zorunlu kılan kanıttır.
|
|
459
|
+
const tumTrackler = [...trackler(aile, 'light'), ...trackler(aile, 'dark')]
|
|
460
|
+
const gecen = KADEMELER.filter((k) => {
|
|
461
|
+
const ham = tokenOku(`${aile}-${k}`, 'light')
|
|
462
|
+
if (!ham) return false
|
|
463
|
+
const d = rgbTokenRgb(ham)
|
|
464
|
+
return tumTrackler.every(([, t]) => rgbKontrast(d, t) >= 3.0)
|
|
465
|
+
})
|
|
466
|
+
expect(gecen).toEqual([])
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it.each(AILELER)('%s track alfası düşürülmedi — /20 ayrımı /10\'dan güçlü', (aile) => {
|
|
470
|
+
// Track'i `/10`'a çekmek dolgu↔track'i kazanırken track↔zemin'i harcardı:
|
|
471
|
+
// barın TOPLAM uzunluğu görünmezleşir.
|
|
472
|
+
const zemin = zeminler('light')[0][1]
|
|
473
|
+
const sem = semantikRgb(aile, 'light')
|
|
474
|
+
expect(rgbKontrast(alfaBileske(sem, 0.2, zemin), zemin)).toBeGreaterThan(
|
|
475
|
+
rgbKontrast(alfaBileske(sem, 0.1, zemin), zemin)
|
|
476
|
+
)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
@@ -124,10 +124,15 @@ describe('Slider Component', () => {
|
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
it('renders error range variant correctly', () => {
|
|
127
|
+
// #442: range light'ta artık `bg-error` DEĞİL — o token track'in
|
|
128
|
+
// (`bg-error/20`) aynısıydı, light'ta ayrım 2.89'a düşüyordu. Semantik
|
|
129
|
+
// token dark render'ı korumak için `dark:` altına taşındı.
|
|
130
|
+
// Gerekçe + kontrast hesabı: slider-warning-error-contrast.test.ts
|
|
127
131
|
render(<Slider rangeVariant="error" data-testid="slider" />)
|
|
128
|
-
|
|
129
|
-
const range = screen.getByTestId('slider').querySelector('
|
|
132
|
+
|
|
133
|
+
const range = screen.getByTestId('slider').querySelector('[class*="--error-700"]')
|
|
130
134
|
expect(range).toBeInTheDocument()
|
|
135
|
+
expect(range).toHaveClass('dark:bg-error')
|
|
131
136
|
})
|
|
132
137
|
})
|
|
133
138
|
|
|
@@ -155,9 +160,12 @@ describe('Slider Component', () => {
|
|
|
155
160
|
|
|
156
161
|
it('renders error thumb variant correctly', () => {
|
|
157
162
|
render(<Slider thumbVariant="error" data-testid="slider" />)
|
|
158
|
-
|
|
163
|
+
|
|
159
164
|
const thumb = screen.getByRole('slider')
|
|
160
|
-
|
|
165
|
+
// #442: kenarlık light'ta -700 kademesine indi, semantik token `dark:`
|
|
166
|
+
// altına taşındı (range ile tek renk). Ölçüm: slider-warning-error-contrast.test.ts
|
|
167
|
+
expect(thumb).toHaveClass('border-[rgb(var(--error-700))]')
|
|
168
|
+
expect(thumb).toHaveClass('dark:border-error')
|
|
161
169
|
})
|
|
162
170
|
})
|
|
163
171
|
|