@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,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #448 — Slider `accent` kontrast değişmezleri (İKİ AYRI EKSEN, İKİ TEMA).
|
|
3
|
+
*
|
|
4
|
+
* BULGU (PR #446'nın kapsam-dışı kardeş bulgusu): `accent` varyantı HER İKİ eksende
|
|
5
|
+
* ve HER İKİ temada eşiğin altındaydı — `warning`/`error`dan (#424/#442) ağır, çünkü
|
|
6
|
+
* onlar yalnız light'ta kusurluydu.
|
|
7
|
+
*
|
|
8
|
+
* EKSEN A (range↔track): range `bg-accent`, track `bg-accent/20` — aynı yüzey
|
|
9
|
+
* token'ının iki alfası. Ölçülen: light 1.123 · dark 1.303 · docs kartı 1.162.
|
|
10
|
+
*
|
|
11
|
+
* EKSEN B (thumb kenarlığı↔zemin): tutamaç `border-accent` + `bg-background`.
|
|
12
|
+
* Dolgu zeminin kendisi olduğundan ayırt edilebilirliği YALNIZ kenarlık taşır.
|
|
13
|
+
* Ölçülen: light 1.154 · dark 1.359 · docs kartı 1.197.
|
|
14
|
+
*
|
|
15
|
+
* Eşik 3:1 (WCAG 1.4.11, non-text). axe yakalamaz: `color-contrast` yalnız METİN ölçer.
|
|
16
|
+
*
|
|
17
|
+
* KÖK NEDEN — YÜZEY token'ı, dolgu rolüne yapısal olarak uygun değil:
|
|
18
|
+
* `--accent` `hover:bg-accent` tinti olarak tasarlanmış ve `.dark`'ta EZİLİYOR (yani
|
|
19
|
+
* #442'nin "tema uyarlaması eksik" kök nedeni burada GEÇERSİZ) — ama iki temada da
|
|
20
|
+
* kendi zeminine bitişik duruyor. Bu yüzden track'i değiştirerek onarmak da mümkün
|
|
21
|
+
* değil; dolgu/kenarlık renginin kendisi değişmek ZORUNDA.
|
|
22
|
+
*
|
|
23
|
+
* ÇÖZÜM SINIFI — #385/#406'nın tek-orta-ton kademe araması AYNEN uygulanamaz:
|
|
24
|
+
* tokens.css'te `--accent-50 … --accent-950` skalası YOK. Sistemin tek nötr skalası
|
|
25
|
+
* slate (`--secondary-*`); `--accent` onunla aynı hue ailesinde (light temada BİREBİR
|
|
26
|
+
* aynı token değeri). Tema-kör tek kademe de seçilemedi — o ailede iki temayı birden
|
|
27
|
+
* geçen tek kademe 500'dür, o da `secondary` varyantının kendi rengi (ΔE*ab = 0.0 →
|
|
28
|
+
* iki varyant klon olurdu). Bu yüzden #442'nin `dark:` ayrımı deseni kullanıldı:
|
|
29
|
+
* range: "bg-[rgb(var(--secondary-700))] dark:bg-[rgb(var(--secondary-400))]"
|
|
30
|
+
* thumb: "border-[rgb(var(--secondary-700))] dark:border-[rgb(var(--secondary-400))] bg-background"
|
|
31
|
+
*
|
|
32
|
+
* NOT: `slider-secondary-contrast.test.ts` (#389/#407) ve
|
|
33
|
+
* `slider-warning-error-contrast.test.ts` (#442) dosyalarından BİLEREK ayrı — her
|
|
34
|
+
* dosya kendi token eksenini kilitler. Pro ikizi:
|
|
35
|
+
* packages/moonui-pro/.../slider-accent-contrast.test.tsx
|
|
36
|
+
*/
|
|
37
|
+
import * as React from 'react'
|
|
38
|
+
import { render } from '@testing-library/react'
|
|
39
|
+
import '@testing-library/jest-dom'
|
|
40
|
+
import * as fs from 'fs'
|
|
41
|
+
import * as path from 'path'
|
|
42
|
+
import { Slider } from '../slider'
|
|
43
|
+
|
|
44
|
+
const KOK = path.resolve(__dirname, '../../..')
|
|
45
|
+
const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
|
|
46
|
+
|
|
47
|
+
/** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
|
|
48
|
+
function tokenOku(ad: string, blok: 'light' | 'dark'): string | null {
|
|
49
|
+
const kaynak =
|
|
50
|
+
blok === 'dark'
|
|
51
|
+
? tokens.slice(tokens.indexOf('.dark'))
|
|
52
|
+
: tokens.slice(0, tokens.indexOf('.dark'))
|
|
53
|
+
return kaynak.match(new RegExp(`--${ad}:\\s*([^;]+);`))?.[1].trim() ?? null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
57
|
+
h /= 360
|
|
58
|
+
s /= 100
|
|
59
|
+
l /= 100
|
|
60
|
+
const f = (n: number) => {
|
|
61
|
+
const k = (n + h * 12) % 12
|
|
62
|
+
const a = s * Math.min(l, 1 - l)
|
|
63
|
+
return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
|
|
64
|
+
}
|
|
65
|
+
return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const hslTokenRgb = (v: string): [number, number, number] => {
|
|
69
|
+
const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
|
|
70
|
+
return hslToRgb(h, s, l)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Skala token'ları ("--secondary-700: 51 65 85") HSL DEĞİL, RGB üçlüsüdür (semantik
|
|
75
|
+
* token'ların HSL formatından farklı) — component'te bu yüzden
|
|
76
|
+
* `rgb(var(--secondary-700))` kompozisyonu kullanılır, hsl() değil.
|
|
77
|
+
*/
|
|
78
|
+
const rgbTokenRgb = (v: string): [number, number, number] => {
|
|
79
|
+
const p = v.split(/\s+/).map((x) => parseFloat(x))
|
|
80
|
+
return [p[0], p[1], p[2]]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function bagilParlaklik([r, g, b]: [number, number, number]): number {
|
|
84
|
+
const c = (v: number) => {
|
|
85
|
+
const x = v / 255
|
|
86
|
+
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
|
|
87
|
+
}
|
|
88
|
+
return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rgbKontrast(a: [number, number, number], b: [number, number, number]): number {
|
|
92
|
+
const la = bagilParlaklik(a)
|
|
93
|
+
const lb = bagilParlaklik(b)
|
|
94
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
95
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* CIE76 ΔE*ab — "iki varyant AYIRT EDİLEBİLİR mi?" sorusunun metriği.
|
|
100
|
+
* WCAG kontrastı bu soruya CEVAP VEREMEZ: yalnız bağıl parlaklık ölçer, bu yüzden
|
|
101
|
+
* apayrı hue'daki iki rengi "aynı" gösterebilir (ölçüldü: `--primary` mavisi ile
|
|
102
|
+
* slate-500 arasındaki kontrast 1.03, ama renkler apayrı). Klon iddiaları bu yüzden
|
|
103
|
+
* ΔE ile kurulur. JND ≈ 2.3; burada kullanılan eşik 10 (rahatça "farklı renk").
|
|
104
|
+
*/
|
|
105
|
+
function rgbToLab([r, g, b]: [number, number, number]): [number, number, number] {
|
|
106
|
+
const s = (v: number) => {
|
|
107
|
+
const x = v / 255
|
|
108
|
+
return (x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)) * 100
|
|
109
|
+
}
|
|
110
|
+
const [R, G, B] = [s(r), s(g), s(b)]
|
|
111
|
+
const X = R * 0.4124 + G * 0.3576 + B * 0.1805
|
|
112
|
+
const Y = R * 0.2126 + G * 0.7152 + B * 0.0722
|
|
113
|
+
const Z = R * 0.0193 + G * 0.1192 + B * 0.9505
|
|
114
|
+
const ref: [number, number, number] = [95.047, 100.0, 108.883]
|
|
115
|
+
const g2 = (t: number) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116)
|
|
116
|
+
const [fx, fy, fz] = [g2(X / ref[0]), g2(Y / ref[1]), g2(Z / ref[2])]
|
|
117
|
+
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function deltaE(a: [number, number, number], b: [number, number, number]): number {
|
|
121
|
+
const A = rgbToLab(a)
|
|
122
|
+
const B = rgbToLab(b)
|
|
123
|
+
return Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `bg-accent/20` gibi alfa katmanının zemin üstündeki bileşke rengi */
|
|
127
|
+
function alfaBileske(
|
|
128
|
+
ust: [number, number, number],
|
|
129
|
+
alfa: number,
|
|
130
|
+
zemin: [number, number, number]
|
|
131
|
+
): [number, number, number] {
|
|
132
|
+
return [
|
|
133
|
+
Math.round(alfa * ust[0] + (1 - alfa) * zemin[0]),
|
|
134
|
+
Math.round(alfa * ust[1] + (1 - alfa) * zemin[1]),
|
|
135
|
+
Math.round(alfa * ust[2] + (1 - alfa) * zemin[2]),
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Docs kabuğunda ölçülen GERÇEK koyu kart zemini (#111827 = rgb(17,24,39)) —
|
|
141
|
+
* #284/#368 konvansiyonu. Bu bulguda DARBOĞAZ: en düşük dark oranlarını o veriyor.
|
|
142
|
+
*/
|
|
143
|
+
const KART_GERCEK_DARK: [number, number, number] = [17, 24, 39]
|
|
144
|
+
|
|
145
|
+
/** Semantik token'ın ilgili temadaki değeri — `.dark` ezmesi YOKSA light'a düşer */
|
|
146
|
+
function semantikRgb(ad: string, blok: 'light' | 'dark'): [number, number, number] {
|
|
147
|
+
return hslTokenRgb(tokenOku(ad, blok) ?? tokenOku(ad, 'light')!)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** O temada slider'ın oturabileceği tüm zeminler */
|
|
151
|
+
function zeminler(blok: 'light' | 'dark'): Array<[string, [number, number, number]]> {
|
|
152
|
+
const z: Array<[string, [number, number, number]]> = [
|
|
153
|
+
['--background', semantikRgb('background', blok)],
|
|
154
|
+
['--card', semantikRgb('card', blok)],
|
|
155
|
+
]
|
|
156
|
+
if (blok === 'dark') z.push(['gerçek kart #111827', KART_GERCEK_DARK])
|
|
157
|
+
return z
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ——— Renkler KAYNAKTAN türetilir ———————————————————————————————————————
|
|
161
|
+
// PR #440'ın dersi: ölçümü "-700/-400" VARSAYIMINA göre yaparsan, biri değeri
|
|
162
|
+
// eskiye döndürdüğünde kriter testleri yeşil kalır (boş-yeşil). Bu yüzden renkler
|
|
163
|
+
// slider.tsx'in O ANKİ CVA sınıf dizgelerinden çözülür — track alfası dahil.
|
|
164
|
+
const kaynak = fs.readFileSync(path.join(KOK, 'components/ui/slider.tsx'), 'utf8')
|
|
165
|
+
const iTrack = kaynak.indexOf('sliderTrackVariants')
|
|
166
|
+
const iRange = kaynak.indexOf('sliderRangeVariants')
|
|
167
|
+
const iThumb = kaynak.indexOf('sliderThumbVariants')
|
|
168
|
+
const iSon = kaynak.indexOf('SliderBaseProps')
|
|
169
|
+
const TRACK_BLOK = kaynak.slice(iTrack, iRange)
|
|
170
|
+
const RANGE_BLOK = kaynak.slice(iRange, iThumb)
|
|
171
|
+
const THUMB_BLOK = kaynak.slice(iThumb, iSon)
|
|
172
|
+
|
|
173
|
+
/** CVA bloğundan `<varyant>: "<sınıflar>"` değerini oku */
|
|
174
|
+
function varyantSinifi(blok: string, varyant: string): string {
|
|
175
|
+
const m = blok.match(new RegExp(`\\n\\s*${varyant}:\\s*"([^"]+)"`))
|
|
176
|
+
if (!m) throw new Error(`${varyant} varyantı bulunamadı`)
|
|
177
|
+
return m[1]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Tek bir Tailwind sınıfını renge çevir (arbitrary skala VEYA semantik token) */
|
|
181
|
+
function sinifRengi(sinif: string, blok: 'light' | 'dark'): [number, number, number] {
|
|
182
|
+
const skala = sinif.match(/^(?:bg|border)-\[rgb\(var\(--([a-z0-9-]+)\)\)\]$/)
|
|
183
|
+
if (skala) return rgbTokenRgb(tokenOku(skala[1], 'light')!) // skala yalnız :root'ta
|
|
184
|
+
const semantik = sinif.match(/^(?:bg|border)-([a-z-]+)$/)
|
|
185
|
+
if (semantik) return semantikRgb(semantik[1], blok)
|
|
186
|
+
throw new Error(`çözülemeyen sınıf: ${sinif}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Varyant dizgesinden o temada GERÇEKTEN uygulanan rengi çöz.
|
|
191
|
+
* `dark:` önekli sınıf dark temada kazanır; `onek` ile eksen seçilir (thumb dizgesi
|
|
192
|
+
* `border-*` yanında `bg-background` da taşır).
|
|
193
|
+
*/
|
|
194
|
+
function uygulananRenk(
|
|
195
|
+
cvaBlok: string,
|
|
196
|
+
varyant: string,
|
|
197
|
+
onek: 'bg' | 'border',
|
|
198
|
+
blok: 'light' | 'dark'
|
|
199
|
+
): [number, number, number] {
|
|
200
|
+
const siniflar = varyantSinifi(cvaBlok, varyant)
|
|
201
|
+
.split(/\s+/)
|
|
202
|
+
.filter((s) => s.replace(/^dark:/, '').startsWith(`${onek}-`))
|
|
203
|
+
const darkSinif = siniflar.find((s) => s.startsWith('dark:'))?.slice('dark:'.length)
|
|
204
|
+
const lightSinif = siniflar.find((s) => !s.startsWith('dark:'))
|
|
205
|
+
if (!lightSinif) throw new Error(`${varyant}/${onek}: temel (light) sınıf yok`)
|
|
206
|
+
return sinifRengi(blok === 'dark' ? darkSinif ?? lightSinif : lightSinif, blok)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const rangeRengi = (v: string, blok: 'light' | 'dark') => uygulananRenk(RANGE_BLOK, v, 'bg', blok)
|
|
210
|
+
const thumbKenarRengi = (v: string, blok: 'light' | 'dark') =>
|
|
211
|
+
uygulananRenk(THUMB_BLOK, v, 'border', blok)
|
|
212
|
+
|
|
213
|
+
/** Track'in her zemin üstündeki bileşkesi — alfa da KAYNAKTAN okunur */
|
|
214
|
+
function trackler(varyant: string, blok: 'light' | 'dark') {
|
|
215
|
+
const sinif = varyantSinifi(TRACK_BLOK, varyant)
|
|
216
|
+
const m = sinif.match(/^bg-([a-z-]+)\/(\d+)$/)
|
|
217
|
+
if (!m) throw new Error(`track sınıfı çözülemedi: ${sinif}`)
|
|
218
|
+
const taban = semantikRgb(m[1], blok)
|
|
219
|
+
const alfa = Number(m[2]) / 100
|
|
220
|
+
return zeminler(blok).map(([ad, z]) => [ad, alfaBileske(taban, alfa, z)] as const)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** `accent` DIŞINDAKİ slider varyantlarının o temada gerçekten uygulanan dolgu renkleri */
|
|
224
|
+
const KARDESLER = ['default', 'primary', 'secondary', 'success', 'warning', 'error'] as const
|
|
225
|
+
const kardesRenkleri = (blok: 'light' | 'dark') =>
|
|
226
|
+
KARDESLER.map((v) => [v, rangeRengi(v, blok)] as const)
|
|
227
|
+
|
|
228
|
+
const KADEMELER = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
229
|
+
/** Ayırt edilebilirlik eşiği (CIE76). Eşiğin yeri sonucu DEĞİŞTİRMİYOR: elenen
|
|
230
|
+
* tek-orta-ton adayı 500, kardeşine ΔE = 0.0 ile birebir AYNI renk. */
|
|
231
|
+
const DELTAE_ESIK = 10
|
|
232
|
+
|
|
233
|
+
/** Slider DOM'u: kök > .moonui-theme > track > range; thumb'lar track'in KARDEŞİ */
|
|
234
|
+
function parcalar(container: HTMLElement) {
|
|
235
|
+
const kabuk = container.querySelector('.moonui-theme') as HTMLElement
|
|
236
|
+
expect(kabuk).not.toBeNull()
|
|
237
|
+
const track = kabuk.firstElementChild as HTMLElement
|
|
238
|
+
const range = track.firstElementChild as HTMLElement
|
|
239
|
+
const thumb = kabuk.querySelector('[role="slider"]') as HTMLElement
|
|
240
|
+
expect(range).not.toBeNull()
|
|
241
|
+
expect(thumb).not.toBeNull()
|
|
242
|
+
return { track, range, thumb }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
describe('#448 hesaplayıcı çapaları', () => {
|
|
246
|
+
it('kontrast hesaplayıcısı DOĞRU — belgelenmiş token çifti beklenen oranı veriyor', () => {
|
|
247
|
+
// tokens.css `--info-subtle` çiftini 9.52:1 diye belgeliyor.
|
|
248
|
+
const oran = rgbKontrast(
|
|
249
|
+
semantikRgb('info-subtle', 'light'),
|
|
250
|
+
semantikRgb('info-subtle-foreground', 'light')
|
|
251
|
+
)
|
|
252
|
+
expect(oran).toBeGreaterThan(9.3)
|
|
253
|
+
expect(oran).toBeLessThan(9.7)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('ΔE hesaplayıcısı DOĞRU — aynı renk 0, siyah↔beyaz 100', () => {
|
|
257
|
+
expect(deltaE([100, 116, 139], [100, 116, 139])).toBeCloseTo(0, 6)
|
|
258
|
+
expect(deltaE([0, 0, 0], [255, 255, 255])).toBeCloseTo(100, 0)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('hesaplayıcı, issue #448 gövdesindeki ölçümleri birebir üretiyor', () => {
|
|
262
|
+
// Çapraz doğrulama: issue tablosu (A: 1.12/1.30/1.16 · B: 1.15/1.36/1.20) bu
|
|
263
|
+
// dosyadan BAĞIMSIZ olarak PR #446 çalışmasında ölçülmüştü. Model onları
|
|
264
|
+
// tutturamıyorsa aşağıdaki tüm oranlar anlamsızdır.
|
|
265
|
+
const eskiA = (blok: 'light' | 'dark') =>
|
|
266
|
+
trackler('accent', blok).map(([, t]) =>
|
|
267
|
+
Number(rgbKontrast(semantikRgb('accent', blok), t).toFixed(2))
|
|
268
|
+
)
|
|
269
|
+
expect(eskiA('light')[0]).toBe(1.12)
|
|
270
|
+
expect(eskiA('dark')[0]).toBe(1.3)
|
|
271
|
+
expect(eskiA('dark')[2]).toBe(1.16) // gerçek docs kartı
|
|
272
|
+
const eskiB = (blok: 'light' | 'dark') =>
|
|
273
|
+
zeminler(blok).map(([, z]) =>
|
|
274
|
+
Number(rgbKontrast(semantikRgb('accent', blok), z).toFixed(2))
|
|
275
|
+
)
|
|
276
|
+
expect(eskiB('light')[0]).toBe(1.15)
|
|
277
|
+
expect(eskiB('dark')[0]).toBe(1.36)
|
|
278
|
+
expect(eskiB('dark')[2]).toBe(1.2)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe('#448 kök neden — `--accent` YÜZEY token’ı, skalası YOK', () => {
|
|
283
|
+
it('`--accent-50 … --accent-950` skalası tokens.css’te TANIMLI DEĞİL', () => {
|
|
284
|
+
// Bu testin varlık sebebi: #385/#406'nın tek-orta-ton kademe araması bir
|
|
285
|
+
// `--accent-*` skalası VARSAYAR. Yok. Biri ileride gerçek bir accent skalası
|
|
286
|
+
// eklerse burası kırmızı olur ve varyantın kendi ailesine döndürülmesi
|
|
287
|
+
// (bu PR'ın önerdiği takip işi) gerektiğini söyler.
|
|
288
|
+
for (const k of KADEMELER) {
|
|
289
|
+
expect(tokenOku(`accent-${k}`, 'light')).toBeNull()
|
|
290
|
+
}
|
|
291
|
+
// `--brand-accent-*` AYRI bir ailedir (marka pembesi), `--accent`in skalası değil.
|
|
292
|
+
expect(tokenOku('brand-accent-500', 'light')).not.toBeNull()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('#442’den FARKLI kök neden: `--accent` `.dark` bloğunda EZİLİYOR', () => {
|
|
296
|
+
// warning/error'ın kusuru "dark ezmesi yok" idi (o yüzden yalnız light'ta
|
|
297
|
+
// kırıktılar). accent tema-uyarlanabilir OLDUĞU HÂLDE iki temada da kırık:
|
|
298
|
+
// kusur uyarlamanın yokluğu değil, token'ın YÜZEY rolü.
|
|
299
|
+
expect(tokenOku('accent', 'dark')).not.toBeNull()
|
|
300
|
+
expect(tokenOku('accent', 'light')).not.toBe(tokenOku('accent', 'dark'))
|
|
301
|
+
expect(tokenOku('warning', 'dark')).toBeNull() // karşıtlık
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('`--accent` iki temada da KENDİ zeminine bitişik (yüzey tinti olduğunun kanıtı)', () => {
|
|
305
|
+
// Track'i değiştirerek onarmanın neden imkânsız olduğunun kanıtı: dolgu rengi
|
|
306
|
+
// zeminden ayrışmıyorsa, altına konacak hiçbir track 3:1 üretemez.
|
|
307
|
+
for (const blok of ['light', 'dark'] as const) {
|
|
308
|
+
for (const [, z] of zeminler(blok)) {
|
|
309
|
+
expect(rgbKontrast(semantikRgb('accent', blok), z)).toBeLessThan(1.5)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('#448 reddedilen alternatifler (seçimin gerekçesi)', () => {
|
|
316
|
+
it('`bg-accent-foreground` ELENDİ — iki temada da `--primary` ile BİREBİR aynı', () => {
|
|
317
|
+
// #389'un `secondary` için verdiği kararın aynısı: varyant primary klonuna dönerdi.
|
|
318
|
+
for (const blok of ['light', 'dark'] as const) {
|
|
319
|
+
expect(tokenOku('accent-foreground', blok)).toBe(tokenOku('primary', blok))
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('`bg-accent-foreground` AYRICA tek başına dolgu olamaz — preset’lerin 2’sinde BEYAZ', () => {
|
|
324
|
+
// Creative + Nature preset'lerinde accentForeground "0 0% 100%": beyaz sayfada
|
|
325
|
+
// beyaz dolgu. Token, accent YÜZEYİNİN mürekkebi; bağımsız dolgu rengi değil.
|
|
326
|
+
const presetler = fs.readFileSync(path.join(KOK, 'lib/theme-presets.ts'), 'utf8')
|
|
327
|
+
const beyazlar = [...presetler.matchAll(/accentForeground:\s*"([^"]+)"/g)].filter(
|
|
328
|
+
(m) => m[1] === '0 0% 100%'
|
|
329
|
+
)
|
|
330
|
+
expect(beyazlar.length).toBeGreaterThanOrEqual(2)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('slate ailesinde TEMA-KÖR geçen tek kademe 500 — o da `secondary` varyantının KLONU', () => {
|
|
334
|
+
// #385/#406/#389'un çözüm şeklinin burada neden uygulanamadığının kanıtı.
|
|
335
|
+
const gecenler = KADEMELER.filter((k) => {
|
|
336
|
+
const ham = tokenOku(`secondary-${k}`, 'light')
|
|
337
|
+
if (!ham) return false
|
|
338
|
+
const c = rgbTokenRgb(ham)
|
|
339
|
+
return (['light', 'dark'] as const).every((blok) =>
|
|
340
|
+
trackler('accent', blok).every(([, t]) => rgbKontrast(c, t) >= 3.0) &&
|
|
341
|
+
zeminler(blok).every(([, z]) => rgbKontrast(c, z) >= 3.0)
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
expect(gecenler).toEqual([500])
|
|
345
|
+
// ...ve 500, `secondary` varyantının o anki rengiyle BİREBİR aynı (ΔE = 0).
|
|
346
|
+
const k500 = rgbTokenRgb(tokenOku('secondary-500', 'light')!)
|
|
347
|
+
expect(deltaE(k500, rangeRengi('secondary', 'light'))).toBeCloseTo(0, 6)
|
|
348
|
+
expect(deltaE(k500, rangeRengi('secondary', 'dark'))).toBeCloseTo(0, 6)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('DARK temada eşiği geçen VE kardeşlerden ayırt edilebilen TEK kademe 400', () => {
|
|
352
|
+
// Seçimin zorunlu olduğunun kanıtı: dark tarafta serbestlik YOK.
|
|
353
|
+
// ≤300 `default`/`primary` ile çakışır, 500 `secondary` klonu, ≥600 eşiğin altı.
|
|
354
|
+
const uygun = KADEMELER.filter((k) => {
|
|
355
|
+
const ham = tokenOku(`secondary-${k}`, 'light')
|
|
356
|
+
if (!ham) return false
|
|
357
|
+
const c = rgbTokenRgb(ham)
|
|
358
|
+
const esik =
|
|
359
|
+
trackler('accent', 'dark').every(([, t]) => rgbKontrast(c, t) >= 3.0) &&
|
|
360
|
+
zeminler('dark').every(([, z]) => rgbKontrast(c, z) >= 3.0)
|
|
361
|
+
const ayirt = kardesRenkleri('dark').every(([, r]) => deltaE(c, r) >= DELTAE_ESIK)
|
|
362
|
+
return esik && ayirt
|
|
363
|
+
})
|
|
364
|
+
expect(uygun).toEqual([400])
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('LIGHT temada {600,700,800} uygun; 700 kardeş ayrımını MAKSİMİZE ediyor', () => {
|
|
368
|
+
const skor = (k: number) => {
|
|
369
|
+
const c = rgbTokenRgb(tokenOku(`secondary-${k}`, 'light')!)
|
|
370
|
+
return Math.min(...kardesRenkleri('light').map(([, r]) => deltaE(c, r)))
|
|
371
|
+
}
|
|
372
|
+
const uygun = KADEMELER.filter((k) => {
|
|
373
|
+
const c = rgbTokenRgb(tokenOku(`secondary-${k}`, 'light')!)
|
|
374
|
+
const esik =
|
|
375
|
+
trackler('accent', 'light').every(([, t]) => rgbKontrast(c, t) >= 3.0) &&
|
|
376
|
+
zeminler('light').every(([, z]) => rgbKontrast(c, z) >= 3.0)
|
|
377
|
+
return esik && skor(k) >= DELTAE_ESIK
|
|
378
|
+
})
|
|
379
|
+
expect(uygun).toEqual([600, 700, 800])
|
|
380
|
+
expect(Math.max(...uygun.map(skor))).toBeCloseTo(skor(700), 6)
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
describe('#448 kaynak sözleşmesi — slider.tsx', () => {
|
|
385
|
+
it('track `accent` sınıfı `/20` alfasını KORUYOR (yedi varyantın ortak deseni)', () => {
|
|
386
|
+
expect(varyantSinifi(TRACK_BLOK, 'accent')).toBe('bg-accent/20')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('EKSEN A: range `accent` light’ta -700, dark’ta -400', () => {
|
|
390
|
+
expect(varyantSinifi(RANGE_BLOK, 'accent')).toBe(
|
|
391
|
+
'bg-[rgb(var(--secondary-700))] dark:bg-[rgb(var(--secondary-400))]'
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('EKSEN B: thumb `accent` kenarlığı light’ta -700, dark’ta -400', () => {
|
|
396
|
+
expect(varyantSinifi(THUMB_BLOK, 'accent')).toBe(
|
|
397
|
+
'border-[rgb(var(--secondary-700))] dark:border-[rgb(var(--secondary-400))] bg-background'
|
|
398
|
+
)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('KAPSAM KİLİDİ: diğer altı varyant DOKUNULMADI', () => {
|
|
402
|
+
// #424/#442 (warning/error) ve #389/#407 (secondary) kararları korunuyor;
|
|
403
|
+
// primary/success/default zaten eşiği geçiyor. Gereksiz değişiklik = regresyon riski.
|
|
404
|
+
expect(varyantSinifi(RANGE_BLOK, 'default')).toBe('bg-foreground')
|
|
405
|
+
expect(varyantSinifi(RANGE_BLOK, 'primary')).toBe('bg-primary')
|
|
406
|
+
expect(varyantSinifi(RANGE_BLOK, 'secondary')).toBe('bg-[rgb(var(--secondary-500))]')
|
|
407
|
+
expect(varyantSinifi(RANGE_BLOK, 'success')).toBe('bg-success')
|
|
408
|
+
expect(varyantSinifi(RANGE_BLOK, 'warning')).toBe(
|
|
409
|
+
'bg-[rgb(var(--warning-700))] dark:bg-warning'
|
|
410
|
+
)
|
|
411
|
+
expect(varyantSinifi(RANGE_BLOK, 'error')).toBe('bg-[rgb(var(--error-700))] dark:bg-error')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('zemin denkliği: `.moonui-theme` paket CSS’inde token yeniden tanımlamıyor', () => {
|
|
415
|
+
// Hesap, track'in/tutamacın altındaki yüzeyin --background/--card olduğunu
|
|
416
|
+
// VARSAYAR. Biri `.moonui-theme`i token kapsamına çevirirse o varsayım sessizce
|
|
417
|
+
// yanlışlanır — o an burası kırmızı olur (#389 deseni).
|
|
418
|
+
const stilDizini = path.join(KOK, 'styles')
|
|
419
|
+
const stiller = fs
|
|
420
|
+
.readdirSync(stilDizini)
|
|
421
|
+
.filter((f) => f.endsWith('.css'))
|
|
422
|
+
.map((f) => fs.readFileSync(path.join(stilDizini, f), 'utf8'))
|
|
423
|
+
.join('\n')
|
|
424
|
+
expect(stiller).not.toMatch(/\.moonui-theme[^{]*\{[^}]*--(accent|secondary|background|card)/)
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
describe('#448 render sözleşmesi — <Slider />', () => {
|
|
429
|
+
it('accent: track eski token’da, range yeni ifadeyle DOM’a iniyor', () => {
|
|
430
|
+
const { container } = render(
|
|
431
|
+
React.createElement(Slider, { trackVariant: 'accent', rangeVariant: 'accent' })
|
|
432
|
+
)
|
|
433
|
+
const { track, range } = parcalar(container)
|
|
434
|
+
expect(track).toHaveClass('bg-accent/20')
|
|
435
|
+
expect(range.className).toContain('bg-[rgb(var(--secondary-700))]')
|
|
436
|
+
expect(range.className).toContain('dark:bg-[rgb(var(--secondary-400))]')
|
|
437
|
+
// Yüzey token'ı artık dolguya UYGULANMAMALI (track'in aynısıydı).
|
|
438
|
+
expect(range.className).not.toMatch(/(?:^|\s)bg-accent(?:\s|$)/)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('accent: thumb kenarlığı yeni ifadeyle iniyor, dolgusu bg-background kalıyor', () => {
|
|
442
|
+
const { container } = render(React.createElement(Slider, { thumbVariant: 'accent' }))
|
|
443
|
+
const { thumb } = parcalar(container)
|
|
444
|
+
expect(thumb.className).toContain('border-[rgb(var(--secondary-700))]')
|
|
445
|
+
expect(thumb.className).toContain('dark:border-[rgb(var(--secondary-400))]')
|
|
446
|
+
expect(thumb.className).toContain('bg-background')
|
|
447
|
+
expect(thumb.className).not.toMatch(/(?:^|\s)border-accent(?:\s|$)/)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('thumbVariant düşüşü KORUNUYOR — tek prop ile track+range+thumb', () => {
|
|
451
|
+
// `trackVariant || thumbVariant || "default"` ve `rangeVariant || thumbVariant ||
|
|
452
|
+
// "primary"` düşüşleri değişmedi; en yaygın kullanım bu yoldan geçer.
|
|
453
|
+
const { container } = render(React.createElement(Slider, { thumbVariant: 'accent' }))
|
|
454
|
+
const { track, range, thumb } = parcalar(container)
|
|
455
|
+
expect(track).toHaveClass('bg-accent/20')
|
|
456
|
+
expect(range.className).toContain('bg-[rgb(var(--secondary-700))]')
|
|
457
|
+
expect(thumb.className).toContain('border-[rgb(var(--secondary-700))]')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('rangeVariant açık verildiğinde thumbVariant’ı hâlâ eziyor', () => {
|
|
461
|
+
const { container } = render(
|
|
462
|
+
React.createElement(Slider, { thumbVariant: 'accent', rangeVariant: 'success' })
|
|
463
|
+
)
|
|
464
|
+
const { range, thumb } = parcalar(container)
|
|
465
|
+
expect(range.className).toContain('bg-success')
|
|
466
|
+
expect(range.className).not.toContain('--secondary-700')
|
|
467
|
+
// Eksenler bağımsız: tutamaç kendi varyantını korur.
|
|
468
|
+
expect(thumb.className).toContain('border-[rgb(var(--secondary-700))]')
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('diğer varyantlara SIZMADI — secondary hâlâ -500, primary hâlâ token’ında', () => {
|
|
472
|
+
const { container: c1 } = render(
|
|
473
|
+
React.createElement(Slider, { rangeVariant: 'secondary', thumbVariant: 'secondary' })
|
|
474
|
+
)
|
|
475
|
+
const p1 = parcalar(c1)
|
|
476
|
+
expect(p1.range.className).toContain('bg-[rgb(var(--secondary-500))]')
|
|
477
|
+
expect(p1.range.className).not.toContain('--secondary-700')
|
|
478
|
+
const { container: c2 } = render(
|
|
479
|
+
React.createElement(Slider, { rangeVariant: 'primary', thumbVariant: 'primary' })
|
|
480
|
+
)
|
|
481
|
+
const p2 = parcalar(c2)
|
|
482
|
+
expect(p2.range.className).toContain('bg-primary')
|
|
483
|
+
expect(p2.thumb.className).toContain('border-primary')
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
describe.each(['light', 'dark'] as const)('#448 %s tema — kabul kriteri ≥3:1 (WCAG 1.4.11)', (blok) => {
|
|
488
|
+
it('EKSEN A (range↔track): TÜM zeminlerde geçiyor', () => {
|
|
489
|
+
// Ölçülen: light 10.081 · dark 7.533 · gerçek docs kartı 6.717
|
|
490
|
+
const dolgu = rangeRengi('accent', blok)
|
|
491
|
+
for (const [ad, track] of trackler('accent', blok)) {
|
|
492
|
+
const oran = rgbKontrast(dolgu, track)
|
|
493
|
+
expect({ eksen: 'A', tema: blok, zemin: ad, gecti: oran >= 3.0 }).toEqual({
|
|
494
|
+
eksen: 'A',
|
|
495
|
+
tema: blok,
|
|
496
|
+
zemin: ad,
|
|
497
|
+
gecti: true,
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('EKSEN B (thumb kenarlığı↔zemin): TÜM zeminlerde geçiyor', () => {
|
|
503
|
+
// Ölçülen: light 10.355 · dark 7.857 · gerçek docs kartı 6.919
|
|
504
|
+
const kenar = thumbKenarRengi('accent', blok)
|
|
505
|
+
for (const [ad, zemin] of zeminler(blok)) {
|
|
506
|
+
const oran = rgbKontrast(kenar, zemin)
|
|
507
|
+
expect({ eksen: 'B', tema: blok, zemin: ad, gecti: oran >= 3.0 }).toEqual({
|
|
508
|
+
eksen: 'B',
|
|
509
|
+
tema: blok,
|
|
510
|
+
zemin: ad,
|
|
511
|
+
gecti: true,
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('ESKİ değer (`bg-accent`/`border-accent`) BU temada başarısızdı — bug’ın belgesi', () => {
|
|
517
|
+
// Düzeltmenin GEREKLİLİĞİNİ sabitler. warning/error'dan farkı: orada yalnız
|
|
518
|
+
// light kırıktı, burada İKİ tema da.
|
|
519
|
+
const eski = semantikRgb('accent', blok)
|
|
520
|
+
for (const [, track] of trackler('accent', blok)) {
|
|
521
|
+
expect(rgbKontrast(eski, track)).toBeLessThan(3.0)
|
|
522
|
+
}
|
|
523
|
+
for (const [, zemin] of zeminler(blok)) {
|
|
524
|
+
expect(rgbKontrast(eski, zemin)).toBeLessThan(3.0)
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it('TEK RENK: tutamaç kenarlığı ile range dolgusu AYNI renk', () => {
|
|
529
|
+
// #407'nin gerekçesi: "tutamaç ve dolgu tek renkte okunur".
|
|
530
|
+
expect(thumbKenarRengi('accent', blok)).toEqual(rangeRengi('accent', blok))
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('KLON DEĞİL: accent, altı kardeş varyanttan da ayırt edilebiliyor (ΔE ≥ 10)', () => {
|
|
534
|
+
// Bu iş sırasında `--secondary-500`e eşlemenin neden REDDEDİLDİĞİNİN kilidi.
|
|
535
|
+
// Biri ileride accent'i secondary'nin kademesine çekerse burası kırmızı olur.
|
|
536
|
+
const dolgu = rangeRengi('accent', blok)
|
|
537
|
+
for (const [ad, renk] of kardesRenkleri(blok)) {
|
|
538
|
+
expect({ kardes: ad, ayirt: deltaE(dolgu, renk) >= DELTAE_ESIK }).toEqual({
|
|
539
|
+
kardes: ad,
|
|
540
|
+
ayirt: true,
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('HUE SADAKATİ: yeni dolgu, `--accent`in nötr slate ailesinde kaldı', () => {
|
|
546
|
+
// Marka pembesi/moru (`--brand-accent-*`, `--brand-*`) eşiği geçen alternatiflerdi
|
|
547
|
+
// ama accent'in hue'sunu (216°) terk ederlerdi. Nötr kalındığının kilidi:
|
|
548
|
+
// R/G/B kanalları arasındaki yayılım dar (doygunluk düşük).
|
|
549
|
+
const [r, g, b] = rangeRengi('accent', blok)
|
|
550
|
+
expect(Math.max(r, g, b) - Math.min(r, g, b)).toBeLessThan(60)
|
|
551
|
+
expect(b).toBeGreaterThan(r) // slate: maviye çalan nötr, `--accent` gibi
|
|
552
|
+
})
|
|
553
|
+
})
|