@moontra/moonui 6.19.1 → 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 +89 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +89 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/slider-accent-contrast.test.ts +553 -0
- 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/slider.tsx +46 -2
- package/src/components/ui/switch.tsx +44 -5
|
@@ -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', () => {
|
|
@@ -72,7 +72,42 @@ const sliderRangeVariants = cva(
|
|
|
72
72
|
// tüketici --secondary-500'ü ezerek özelleştirebilir.
|
|
73
73
|
// Regresyon: __tests__/slider-secondary-contrast.test.ts
|
|
74
74
|
secondary: "bg-[rgb(var(--secondary-500))]",
|
|
75
|
-
accent:
|
|
75
|
+
// #448 (#389/#407'nin `accent` kardeşi — ama ÇÖZÜM SINIFI farklı): range
|
|
76
|
+
// `bg-accent`, track `bg-accent/20` idi — aynı yüzey token'ının iki alfası.
|
|
77
|
+
// `--accent` HER İKİ temada da kusurluydu (warning/error'dan ağır: onlar
|
|
78
|
+
// yalnız light'ta kusurluydu): A ekseni light 1.123 · dark 1.303 · gerçek
|
|
79
|
+
// docs kartı (#111827) 1.162. Eşik 3:1 (WCAG 1.4.11 non-text).
|
|
80
|
+
//
|
|
81
|
+
// KÖK NEDEN: `--accent` bir YÜZEY token'ı — `hover:bg-accent` tinti olarak
|
|
82
|
+
// tasarlanmış ve iki temada da kendi zeminine BİTİŞİK duruyor (light
|
|
83
|
+
// rgb(235,239,245) beyaz üstünde, dark rgb(29,40,58) rgb(3,7,17) üstünde).
|
|
84
|
+
// Dolgu/kenarlık rolüne yapısal olarak uygun değil; bu yüzden track'i
|
|
85
|
+
// değiştirerek onarmak da MÜMKÜN DEĞİL (light'ta accent'ten 3:1 uzakta
|
|
86
|
+
// duran daha açık bir yüzey yok).
|
|
87
|
+
//
|
|
88
|
+
// #389'un tek-orta-ton kademe araması AYNEN uygulanamadı: tokens.css'te
|
|
89
|
+
// `--accent-50 … --accent-950` skalası YOK (skalası olan aileler:
|
|
90
|
+
// primary/secondary/success/warning/error/brand/brand-accent).
|
|
91
|
+
// `bg-accent-foreground` de elendi — `--accent-foreground` iki temada da
|
|
92
|
+
// `--primary` ile BİREBİR aynı değer (varyant primary klonuna dönerdi) ve
|
|
93
|
+
// 7 tema preset'inin 2'sinde beyaz (Creative/Nature), yani tek başına
|
|
94
|
+
// dolgu olamaz.
|
|
95
|
+
//
|
|
96
|
+
// ÇÖZÜM: sistemin TEK nötr skalası olan slate (`--secondary-*`) — `--accent`
|
|
97
|
+
// ile aynı hue ailesi (216° ↔ 215°), üstelik light temada `--accent` ile
|
|
98
|
+
// `--secondary` BİREBİR aynı token değerini taşıyor. Tema-kör tek kademe
|
|
99
|
+
// SEÇİLEMEDİ: bu ailede iki temayı birden geçen tek kademe 500'dür, o da
|
|
100
|
+
// `secondary` varyantının kendi rengi (ΔE*ab = 0.0 — iki varyant klon olurdu).
|
|
101
|
+
// Bu yüzden #442'nin `dark:` ayrımı deseni kullanıldı, tema başına ayrı kademe.
|
|
102
|
+
// Tam tarama (11 kademe × 2 tema × 3 zemin × 2 eksen): DARK'ta eşiği geçen VE
|
|
103
|
+
// kardeş varyantlardan ayırt edilebilen (ΔE*ab ≥ 10) TEK kademe 400
|
|
104
|
+
// (500 = secondary klonu · ≤300 `default`/`primary` ile çakışıyor · ≥600 eşiğin
|
|
105
|
+
// altında). LIGHT'ta {600,700,800} uygun; 700 kardeş ayrımını maksimize eder
|
|
106
|
+
// (ΔE 21.2 · 600→12.7 · 800→14.9).
|
|
107
|
+
// Ölçülen — A: light 1.123 → 10.081 ✓ · dark 1.303 → 7.533 ✓ · docs kartı
|
|
108
|
+
// 1.162 → 6.717 ✓. Track `bg-accent/20` DEĞİŞMEDİ (yedi varyantın ortak deseni).
|
|
109
|
+
// Regresyon: __tests__/slider-accent-contrast.test.ts
|
|
110
|
+
accent: "bg-[rgb(var(--secondary-700))] dark:bg-[rgb(var(--secondary-400))]",
|
|
76
111
|
success: "bg-success",
|
|
77
112
|
// #442 (#424/PR #440'ın slider ikizi): range `bg-warning`/`bg-error` idi —
|
|
78
113
|
// track (`bg-warning/20` · `bg-error/20`) AYNI token'ın alfalı hâli olduğundan
|
|
@@ -128,7 +163,16 @@ const sliderThumbVariants = cva(
|
|
|
128
163
|
// aynı kademe: tutamaç ve dolgu tek renkte okunur.
|
|
129
164
|
// Regresyon: __tests__/slider-secondary-contrast.test.ts
|
|
130
165
|
secondary: "border-[rgb(var(--secondary-500))] bg-background",
|
|
131
|
-
|
|
166
|
+
// #448 EKSEN B (range'den FARKLI eksen: bileşen↔sayfa-zemini) — tutamacın
|
|
167
|
+
// dolgusu `bg-background`, yani zeminin kendisi; ayırt edilebilirliğini
|
|
168
|
+
// YALNIZ kenarlık taşır → non-text UI bileşeni, WCAG 1.4.11 ≥3:1.
|
|
169
|
+
// `border-accent` iki temada da kayboluyordu: light 1.154 · dark 1.359 ·
|
|
170
|
+
// gerçek docs kartı 1.197. Range ile AYNI kademeler seçildi — #407'nin
|
|
171
|
+
// gerekçesi burada da geçerli: "tutamaç ve dolgu tek renkte okunur".
|
|
172
|
+
// Ölçülen — light 1.154 → 10.355 ✓ · dark 1.359 → 7.857 ✓ · docs kartı
|
|
173
|
+
// 1.197 → 6.919 ✓.
|
|
174
|
+
// Regresyon: __tests__/slider-accent-contrast.test.ts
|
|
175
|
+
accent: "border-[rgb(var(--secondary-700))] dark:border-[rgb(var(--secondary-400))] bg-background",
|
|
132
176
|
success: "border-success bg-background",
|
|
133
177
|
// #442 EKSEN B (range'den FARKLI eksen: bileşen↔sayfa-zemini) — #407'de
|
|
134
178
|
// `secondary` için açılan eksenin warning/error karşılığı. Tutamacın dolgusu
|
|
@@ -34,12 +34,51 @@ const switchVariants = cva(
|
|
|
34
34
|
md: "h-6 w-11",
|
|
35
35
|
lg: "h-7 w-14",
|
|
36
36
|
},
|
|
37
|
+
// #454 — checked track'in İKİ ekseni de ölçüldü (eşik 3:1, WCAG 1.4.11
|
|
38
|
+
// non-text UI). Switch'te bu iki eksen AYNI sayıyı verir, çünkü thumb
|
|
39
|
+
// `bg-background` taşır (switchThumbVariants tabanı):
|
|
40
|
+
// A) checked track ↔ sayfa/kart zemini → kontrol "açık" okunuyor mu?
|
|
41
|
+
// B) thumb ↔ checked track → tutamacın KONUMU görülüyor mu?
|
|
42
|
+
// Ölçüm zeminleri: `--background` · `--card` · docs kabuğunun gerçek koyu
|
|
43
|
+
// kartı (#111827). Regresyon: __tests__/switch-variant-contrast.test.tsx
|
|
37
44
|
variant: {
|
|
38
|
-
primary: "data-[state=checked]:bg-primary",
|
|
39
|
-
success: "data-[state=checked]:bg-success",
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
primary: "data-[state=checked]:bg-primary", // light 4.920 · dark 19.256 ✓
|
|
46
|
+
success: "data-[state=checked]:bg-success", // light 4.904 · dark 6.043 ✓
|
|
47
|
+
// #454 kardeş kusuru (#424/PR #440 progress · #442/PR #446 slider ile AYNI
|
|
48
|
+
// sınıf, üçüncü component): `--warning` tokens.css'in `.dark` bloğunda
|
|
49
|
+
// EZİLMİYOR, yani iki temada da aynı turuncu. Dark'ta zemin koyu olduğu
|
|
50
|
+
// için geçiyor (9.446), LIGHT'ta beyaz üstünde 2.133 ✗ — açık temada
|
|
51
|
+
// checked warning switch'in tutamacı da kayboluyordu (B ekseni 2.133).
|
|
52
|
+
// ÇÖZÜM (emsalle birebir aynı şekil): eksik tema-uyarlamasını component
|
|
53
|
+
// düzeyinde ver — light'ta skalanın -700 kademesine in, dark'ta semantik
|
|
54
|
+
// token'a DOKUNMA (dark render bit-bit aynı kalır, regresyon yüzeyi sıfır).
|
|
55
|
+
// Light'ta eşiği geçen kademe kümesi {700,800,900,950}; en açık olanı
|
|
56
|
+
// seçildi (turuncu kimliğini en çok koruyan). Ölçülen: 2.133 → 4.923 ✓.
|
|
57
|
+
warning:
|
|
58
|
+
"data-[state=checked]:bg-[rgb(var(--warning-700))] dark:data-[state=checked]:bg-warning",
|
|
59
|
+
destructive: "data-[state=checked]:bg-error", // light 3.781 · dark 5.328 ✓ (dokunulmadı)
|
|
60
|
+
// #454 asıl kusuru: `secondary` varyantı `bg-accent` kullanıyordu — ADI
|
|
61
|
+
// `secondary`, TOKEN'ı `accent`. `--accent` bir YÜZEY token'ı (`hover:bg-accent`
|
|
62
|
+
// tinti) ve iki temada da kendi zeminine bitişik: light rgb(235,239,245)
|
|
63
|
+
// beyaz üstünde 1.154 · dark rgb(29,40,58) 1.359 · docs kartı 1.197.
|
|
64
|
+
// Yani checked switch hem sayfadan hem de kendi tutamacından ayrışmıyordu:
|
|
65
|
+
// "açık" durum GÖRÜNMÜYORDU (switch'in tek işi durumu göstermek).
|
|
66
|
+
//
|
|
67
|
+
// AD ↔ TOKEN KARARI: varyant kendi ailesine, `--secondary-*` slate skalasına
|
|
68
|
+
// bağlandı. `bg-secondary` (semantik) BİLEREK seçilmedi — light'ta `--accent`
|
|
69
|
+
// ile BİREBİR aynı değer (1.154), dark'ta daha da kötü (1.007). Skala
|
|
70
|
+
// taraması (11 kademe × 2 tema × 3 zemin × 2 eksen + kardeş ΔE*ab ≥ 10 klon
|
|
71
|
+
// filtresi): light {500,600,700,800,900,950} · dark {300,400,500} → iki
|
|
72
|
+
// temayı birden geçen TEK kademe **500**, yani `dark:` ayrımına gerek yok.
|
|
73
|
+
// #448'in slider'daki 700/400 ayrımı burada GEREKMEZ ve İSTENMEZ: orada 500
|
|
74
|
+
// elenmişti çünkü slider'ın `secondary` varyantı zaten o rengi kullanıyordu
|
|
75
|
+
// (ΔE = 0.0 → klon). Switch'in `accent` adlı bir varyantı YOK, dolayısıyla
|
|
76
|
+
// 500 serbest ve kardeşlerden ayrık (ΔE: primary 69.5/51.6 · success
|
|
77
|
+
// 63.6/74.8 · warning 96.0 · destructive 85.0 — hepsi ≥ 10).
|
|
78
|
+
// Yan fayda: slider'ın `secondary` varyantı da #389/PR #406'da aynı kademeye
|
|
79
|
+
// sabitlenmişti → aynı varyant adı artık iki component'te aynı rengi veriyor.
|
|
80
|
+
// Ölçülen: A/B light 1.154 → 4.759 ✓ · dark 1.359 → 4.234 ✓ · kart 1.197 → 3.728 ✓.
|
|
81
|
+
secondary: "data-[state=checked]:bg-[rgb(var(--secondary-500))]",
|
|
43
82
|
},
|
|
44
83
|
},
|
|
45
84
|
defaultVariants: {
|