@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.
@@ -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-accent')
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', () => {