@moontra/moonui 6.28.0 → 6.29.0
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/cdn/moonui.css +2 -2
- package/dist/cdn/moonui.esm.js +3 -3
- package/dist/cdn/moonui.global.js +3 -3
- package/dist/index.js +157 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +157 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/issue-646-dark-scope-override.test.tsx +19 -2
- package/src/__tests__/issue-708-badge-outline-override.test.tsx +319 -0
- package/src/__tests__/issue-715-button-override-hover.test.tsx +631 -0
- package/src/components/ui/__tests__/select.test.tsx +13 -3
- package/src/components/ui/badge.tsx +25 -1
- package/src/components/ui/button.tsx +131 -14
- package/src/components/ui/select.tsx +1 -1
- package/src/styles/tokens.css +45 -1
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #715 — button: gereksiz `dark:` tekrarı tüketici override'ını eziyordu (1.28:1)
|
|
3
|
+
* + `destructive`/`primary`/`success` hover/active alfa kompoziti
|
|
4
|
+
* AA altındaydı.
|
|
5
|
+
*
|
|
6
|
+
* ══ Dal 1 — `dark:` tekrarı × twMerge × özgüllük ═══════════════════════════════
|
|
7
|
+
* `cn()` = `twMerge(clsx(...))`. Tüketici `className="text-white"` yazdığında
|
|
8
|
+
* twMerge ÖNEKSİZ `text-primary-foreground`u eler — ama `dark:text-primary-foreground`
|
|
9
|
+
* AYRI bir modifier grubu olduğu için ELENMEZ ve koyu temada özgüllükle kazanır:
|
|
10
|
+
* .dark\:text-primary-foreground:is(.dark *) → (0,2,0)
|
|
11
|
+
* text-white → (0,1,0)
|
|
12
|
+
* Dark `--primary-foreground` = `222.2 47.4% 1.2%` = rgb(2,2,5) → ölçülen 1.28:1.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ AYRIM (bu testin ASIL işi): "tüm `dark:` satırlarını sil" YANLIŞ olurdu.
|
|
15
|
+
* · `dark:text-primary-foreground` → AYNI token'ın tekrarı, token `.dark` bloğunda
|
|
16
|
+
* zaten farklı değer alıyor (tokens.css:137 ↔ :546) → KALDIRILDI.
|
|
17
|
+
* · `dark:bg-error-600` → FARKLI bir token'a geçiyor (#544) → KORUNDU.
|
|
18
|
+
* · `dark:hover:bg-error-700` → öneksiziyle AYNI değeri taşır ama SIRALAMA
|
|
19
|
+
* GUARD'ıdır (aşağıdaki "ordering" testi bunu kanıtla kilitler) → KORUNDU.
|
|
20
|
+
*
|
|
21
|
+
* ══ Dal 2 — durum-bağımlı dallar ═══════════════════════════════════════════════
|
|
22
|
+
* #544 yalnız RESTING ölçmüştü. Açık temada `/90` ve `/80` zemini BEYAZLA
|
|
23
|
+
* karıştırıp rengi AÇIYOR (metin sabit `--destructive-foreground` #f8fafc):
|
|
24
|
+
* resting bg-destructive #e11313 → 4.66 ✓
|
|
25
|
+
* hover bg-destructive/90 #e42b2b → 4.29 ✗ (A11-V axe: 4.28 serious)
|
|
26
|
+
* active bg-destructive/80 #e74242 → 3.82 ✗ (hiç kimse ölçmemişti)
|
|
27
|
+
* Koyu dalın ZATEN kullandığı çözüm açık dala da uygulandı: alfa yerine opak
|
|
28
|
+
* `error` kademesi → hover -700 (6.18), active -800 (7.94).
|
|
29
|
+
*
|
|
30
|
+
* ══ Dal 3 — aynı kusur `primary` ve `success`te ═════════════════════════════════
|
|
31
|
+
* Dal 2 yalnız `destructive`i onardı; ölçüm genişletilince ikisi daha çıktı:
|
|
32
|
+
* primary hover /90 #2876f5 → 4.01 ✗ | active /80 #4085f6 → 3.40 ✗
|
|
33
|
+
* success hover /90 #2a8f4f → 3.90 ✗ | active /80 #419b62 → 3.30 ✗
|
|
34
|
+
* AYNI kusur, FARKLI çözüm — çünkü skala aileleri farklı:
|
|
35
|
+
* · `--success-NNN` `--success` ile aynı hue → Dal 2'nin reçetesi aynen geçerli:
|
|
36
|
+
* hover `bg-success-800` (6.81), active `bg-success-900` (8.71).
|
|
37
|
+
* · `--primary-NNN` ise SKY paleti (`--primary` BLUE) VE preset onları hiçbir
|
|
38
|
+
* utility'ye bağlamıyor → `bg-primary-700` hiç CSS üretmeyen ölü sınıf (#713).
|
|
39
|
+
* Bu yüzden `primary` koyu tonu `color-mix` ile TOKEN'IN KENDİSİNDEN türetir;
|
|
40
|
+
* tüketici `--primary`yi override edince hover/active kendiliğinden uyar.
|
|
41
|
+
* Koyu tema İKİSİNDE DE zaten geçiyordu → ayrıca ölçülüp regresyon kilidi kondu.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ RENKLER VARSAYILMAZ: tüm değerler `tokens.css`ten, tüm sınıflar `button.tsx`
|
|
44
|
+
* kaynağından okunur. Test, koda değil ÖLÇÜME dayanır (#544 ile aynı yöntem).
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ jsdom Tailwind kaskadını çözmez → burada `getComputedStyle` ile RENK ölçülmez.
|
|
47
|
+
* Ölçülen şey MEKANİZMA: (a) hangi sınıflar sağ kalıyor (gerçek `cn()`/twMerge ile),
|
|
48
|
+
* (b) sağ kalan sınıfların token değerleriyle WCAG oranı. Gerçek tarayıcı doğrulaması
|
|
49
|
+
* bu dosyanın kapsamı DIŞINDADIR ve raporda ayrıca belirtilir.
|
|
50
|
+
*/
|
|
51
|
+
import * as React from 'react'
|
|
52
|
+
import { render } from '@testing-library/react'
|
|
53
|
+
import '@testing-library/jest-dom'
|
|
54
|
+
import * as fs from 'fs'
|
|
55
|
+
import * as path from 'path'
|
|
56
|
+
import { Button } from '../components/ui/button'
|
|
57
|
+
|
|
58
|
+
const PKG = path.resolve(__dirname, '..')
|
|
59
|
+
const DEPO = path.resolve(__dirname, '../../../..')
|
|
60
|
+
const tokens = fs.readFileSync(path.join(PKG, 'styles/tokens.css'), 'utf8')
|
|
61
|
+
const freeSrc = fs.readFileSync(path.join(PKG, 'components/ui/button.tsx'), 'utf8')
|
|
62
|
+
const proSrc = fs.readFileSync(
|
|
63
|
+
path.join(DEPO, 'packages/moonui-pro/src/components/ui/button.tsx'),
|
|
64
|
+
'utf8'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
type RGB = [number, number, number]
|
|
68
|
+
type Blok = 'light' | 'dark'
|
|
69
|
+
|
|
70
|
+
// ─── Renk aritmetiği (#544 testiyle birebir aynı yöntem) ────────────────────────
|
|
71
|
+
|
|
72
|
+
function tokenOku(name: string, blok: Blok): string | null {
|
|
73
|
+
const source =
|
|
74
|
+
blok === 'dark' ? tokens.slice(tokens.indexOf('.dark')) : tokens.slice(0, tokens.indexOf('.dark'))
|
|
75
|
+
return source.match(new RegExp(`--${name}:\\s*([^;]+);`))?.[1].trim() ?? null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
79
|
+
h /= 360
|
|
80
|
+
s /= 100
|
|
81
|
+
l /= 100
|
|
82
|
+
const f = (n: number) => {
|
|
83
|
+
const k = (n + h * 12) % 12
|
|
84
|
+
const a = s * Math.min(l, 1 - l)
|
|
85
|
+
return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
|
|
86
|
+
}
|
|
87
|
+
return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hslTokenRgb = (v: string): RGB => {
|
|
91
|
+
const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
|
|
92
|
+
return hslToRgb(h, s, l)
|
|
93
|
+
}
|
|
94
|
+
/** Skala token'ları ("--error-600: 220 38 38") HSL DEĞİL, RGB üçlüsüdür */
|
|
95
|
+
const rgbTokenRgb = (v: string): RGB => {
|
|
96
|
+
const p = v.split(/\s+/).map((x) => parseFloat(x))
|
|
97
|
+
return [p[0], p[1], p[2]]
|
|
98
|
+
}
|
|
99
|
+
const semanticRgb = (name: string, blok: Blok): RGB =>
|
|
100
|
+
hslTokenRgb(tokenOku(name, blok) ?? tokenOku(name, 'light')!)
|
|
101
|
+
/** Skala kademeleri tema-BAĞIMSIZ: `.dark` bloğu onları ezmiyor */
|
|
102
|
+
const scaleRgb = (name: string): RGB => rgbTokenRgb(tokenOku(name, 'light')!)
|
|
103
|
+
|
|
104
|
+
function relativeLuminance([r, g, b]: RGB): number {
|
|
105
|
+
const c = (v: number) => {
|
|
106
|
+
const x = v / 255
|
|
107
|
+
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
|
|
108
|
+
}
|
|
109
|
+
return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
|
|
110
|
+
}
|
|
111
|
+
function kontrast(a: RGB, b: RGB): number {
|
|
112
|
+
const la = relativeLuminance(a)
|
|
113
|
+
const lb = relativeLuminance(b)
|
|
114
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
115
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
116
|
+
}
|
|
117
|
+
/** Yarı saydam rengi opak zemine bindir — `bg-destructive/90` gibi sınıflar için ŞART */
|
|
118
|
+
const komposit = (ust: RGB, alt: RGB, alfa: number): RGB =>
|
|
119
|
+
ust.map((c, i) => Math.round(c * alfa + alt[i] * (1 - alfa))) as RGB
|
|
120
|
+
|
|
121
|
+
const hex = ([r, g, b]: RGB): string =>
|
|
122
|
+
'#' + [r, g, b].map((x) => Math.round(x).toString(16).padStart(2, '0')).join('')
|
|
123
|
+
|
|
124
|
+
// ─── Zeminler — HER ÖLÇÜMDE ADIYLA ANILIR ───────────────────────────────────────
|
|
125
|
+
/** Açık temada butonun ALTINDAKİ yüzey. A11-V'nin axe ölçümünü aldığı zemin. */
|
|
126
|
+
const WHITE: RGB = [255, 255, 255]
|
|
127
|
+
/** Docs kabuğunda ölçülen GERÇEK koyu kart (#284/#368/#448/#454 konvansiyonu) */
|
|
128
|
+
const CARD_ACTUAL_DARK: RGB = [17, 24, 39]
|
|
129
|
+
|
|
130
|
+
const AA_TEXT = 4.5
|
|
131
|
+
|
|
132
|
+
// ─── Varyant sınıflarını KAYNAKTAN çöz ─────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/** cva `variant` bloğundaki dizi biçimli girdiyi birleşik sınıf dizgesi olarak ver */
|
|
135
|
+
function variantClass(source: string, variantVal: string): string {
|
|
136
|
+
const m = source.match(new RegExp(`\\n\\s*${variantVal}:\\s*\\[([\\s\\S]*?)\\n\\s*\\]`))
|
|
137
|
+
if (!m) throw new Error(`${variantVal} varyantı bulunamadı`)
|
|
138
|
+
return (m[1].match(/"([^"]*)"/g) ?? []).map((s) => s.slice(1, -1)).join(' ')
|
|
139
|
+
}
|
|
140
|
+
const variantClasses = (source: string, v: string): string[] =>
|
|
141
|
+
variantClass(source, v).split(/\s+/).filter(Boolean)
|
|
142
|
+
|
|
143
|
+
const VARYANTLAR = [
|
|
144
|
+
'primary', 'secondary', 'outline', 'ghost', 'destructive',
|
|
145
|
+
'success', 'link', 'gradient', 'glow', 'soft', 'glass', 'neon',
|
|
146
|
+
] as const
|
|
147
|
+
|
|
148
|
+
const BLACK: RGB = [0, 0, 0]
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Dal 3 — `bg-[color:color-mix(in_srgb,hsl(var(--primary))_85%,black)]`.
|
|
152
|
+
*
|
|
153
|
+
* `color-mix(in srgb, …)` gama-KODLU sRGB koordinatlarında interpolasyon yapar
|
|
154
|
+
* (CSS Color 4) → `komposit()`in kanal-bazlı lerp'i ile BİREBİR aynı aritmetik.
|
|
155
|
+
* Alfadan farkı: ikinci renk SAYFA ZEMİNİ değil siyah, yani sonuç opak ve
|
|
156
|
+
* kontrast artık altındaki yüzeye BAĞIMLI DEĞİL. Kusurun kökü buydu.
|
|
157
|
+
*/
|
|
158
|
+
const COLOR_MIX_RE =
|
|
159
|
+
/^bg-\[color:color-mix\(in_srgb,hsl\(var\(--([a-z-]+)\)\)_(\d{1,3})%,black\)\]$/
|
|
160
|
+
|
|
161
|
+
/** `bg-error-700` / `bg-destructive` / `bg-destructive/90` / `bg-[color:color-mix(…)]` → RGB */
|
|
162
|
+
function classColor(cls: string, blok: Blok, background: RGB): RGB {
|
|
163
|
+
const g = cls.replace(/^(?:(?:dark|hover|active|focus-visible|group-hover):)+/, '')
|
|
164
|
+
const mix = g.match(COLOR_MIX_RE)
|
|
165
|
+
if (mix) return komposit(semanticRgb(mix[1], blok), BLACK, Number(mix[2]) / 100)
|
|
166
|
+
const m = g.match(/^(?:bg|text|border|ring)-([a-z-]+?)(?:-(\d{2,3}))?(?:\/(\d{1,3}))?$/)
|
|
167
|
+
if (!m) throw new Error(`çözülemeyen sınıf: ${cls}`)
|
|
168
|
+
const [, name, kademe, alfa] = m
|
|
169
|
+
if (name === 'white') return alfa ? komposit(WHITE, background, Number(alfa) / 100) : WHITE
|
|
170
|
+
const base = kademe ? scaleRgb(`${name}-${kademe}`) : semanticRgb(name, blok)
|
|
171
|
+
return alfa ? komposit(base, background, Number(alfa) / 100) : base
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Bir varyantın ilgili tema+durumdaki ETKİN dolgusu.
|
|
176
|
+
* `dark:` önekli sınıf koyu temada kazanır (Tailwind `dark:`i `hover:`ten SONRA
|
|
177
|
+
* yazar; ayrıca `dark:hover:` (0,3,0) `dark:` (0,2,0)'ı yener).
|
|
178
|
+
*/
|
|
179
|
+
function fill(source: string, variantVal: string, blok: Blok, background: RGB, onek = ''): RGB {
|
|
180
|
+
const adaylar = variantClasses(source, variantVal).filter((c) =>
|
|
181
|
+
new RegExp(`^${blok === 'dark' ? '(?:dark:)?' : '(?!dark:)'}${onek}bg-`).test(c)
|
|
182
|
+
)
|
|
183
|
+
const secili =
|
|
184
|
+
blok === 'dark'
|
|
185
|
+
? adaylar.find((c) => c.startsWith(`dark:${onek}bg-`)) ?? adaylar[0]
|
|
186
|
+
: adaylar[0]
|
|
187
|
+
if (!secili) throw new Error(`${variantVal}/${blok}/"${onek}" için dolgu sınıfı yok`)
|
|
188
|
+
return classColor(secili, blok, background)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Varyantın ilgili temadaki ETKİN metin rengi */
|
|
192
|
+
function textColor(source: string, variantVal: string, blok: Blok, background: RGB): RGB {
|
|
193
|
+
const adaylar = variantClasses(source, variantVal).filter((c) =>
|
|
194
|
+
/^(?:dark:)?text-/.test(c) && !/^(?:dark:)?(?:hover|active|focus-visible):/.test(c)
|
|
195
|
+
)
|
|
196
|
+
const secili =
|
|
197
|
+
blok === 'dark' ? adaylar.find((c) => c.startsWith('dark:')) ?? adaylar[0] : adaylar.find((c) => !c.startsWith('dark:'))
|
|
198
|
+
if (!secili) throw new Error(`${variantVal}/${blok} için metin sınıfı yok`)
|
|
199
|
+
return classColor(secili, blok, background)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const cls = (jsx: React.ReactElement) => {
|
|
203
|
+
const { container } = render(jsx)
|
|
204
|
+
return (container.querySelector('button') as HTMLElement).className
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const PAKETLER = [
|
|
208
|
+
['free', freeSrc],
|
|
209
|
+
['pro', proSrc],
|
|
210
|
+
] as const
|
|
211
|
+
|
|
212
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
213
|
+
// Dal 1 — gereksiz `dark:` tekrarı tüketici override'ını eziyordu
|
|
214
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
215
|
+
|
|
216
|
+
describe('#715 Dal 1 — `primary` artık hiçbir `dark:` sınıfı taşımıyor', () => {
|
|
217
|
+
it.each(PAKETLER)('%s: `primary` dizisinde `dark:` YOK (token zaten tema-duyarlı)', (_ad, src) => {
|
|
218
|
+
const dark = variantClasses(src, 'primary').filter((c) => c.startsWith('dark:'))
|
|
219
|
+
expect(dark).toEqual([])
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it.each(PAKETLER)('%s: öneksiz çift KORUNDU — tema davranışı değişmedi', (_ad, src) => {
|
|
223
|
+
expect(variantClass(src, 'primary')).toContain('bg-primary text-primary-foreground')
|
|
224
|
+
// Dal 3 hover/active dallarını `/90`-`/80` alfasından `color-mix`e çevirdi;
|
|
225
|
+
// ikisi de HÂLÂ öneksiz (tek sınıf, iki tema) — Dal 1'in kazanımı bozulmadı.
|
|
226
|
+
expect(variantClasses(src, 'primary')).toEqual(
|
|
227
|
+
expect.arrayContaining([
|
|
228
|
+
'hover:bg-[color:color-mix(in_srgb,hsl(var(--primary))_85%,black)]',
|
|
229
|
+
'active:bg-[color:color-mix(in_srgb,hsl(var(--primary))_75%,black)]',
|
|
230
|
+
])
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it.each(PAKETLER)('%s: kaynakta `dark:text-primary-foreground` hiç kalmadı', (_ad, src) => {
|
|
235
|
+
// Yorum satırları issue'yu anlatmak için sınıf adını LİTERAL içeriyor →
|
|
236
|
+
// yorumsuz gövde üzerinden bak (yoksa test kendi belgesine takılır).
|
|
237
|
+
const yorumsuz = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
238
|
+
expect(yorumsuz).not.toContain('dark:text-primary-foreground')
|
|
239
|
+
expect(yorumsuz).not.toContain('dark:bg-primary ')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('render: `primary` çıktısı `dark:text-primary-foreground` İÇERMEZ', () => {
|
|
243
|
+
const c = cls(<Button variant="primary">x</Button>)
|
|
244
|
+
expect(c).toContain('text-primary-foreground')
|
|
245
|
+
expect(c).not.toContain('dark:text-primary-foreground')
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe('#715 Dal 1 — tüketici override\'ı artık EZİLMİYOR', () => {
|
|
250
|
+
it('`className="text-white"` sağ kalıyor ve onu yenecek `dark:text-*` yok', () => {
|
|
251
|
+
const c = cls(<Button className="text-white">x</Button>)
|
|
252
|
+
expect(c.split(/\s+/)).toContain('text-white')
|
|
253
|
+
// Kusurun imzası: twMerge öneksizi eler, `dark:` ikizi kalır ve (0,2,0) ile
|
|
254
|
+
// tüketicinin (0,1,0) sınıfını yener. Hiç `dark:text-*` kalmadıysa mümkün değil.
|
|
255
|
+
expect(c.split(/\s+/).filter((x) => x.startsWith('dark:text-'))).toEqual([])
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('A13-V\'nin ölçtüğü GERÇEK docs vakası (click-animations gradyan butonu)', () => {
|
|
259
|
+
// src/docs-config/click-animations.config.tsx:363 — birebir aynı className
|
|
260
|
+
const c = cls(
|
|
261
|
+
<Button
|
|
262
|
+
size="lg"
|
|
263
|
+
className="bg-gradient-to-r from-indigo-600 to-purple-600 text-white hover:from-indigo-700 hover:to-purple-700"
|
|
264
|
+
>
|
|
265
|
+
Ripple Effect
|
|
266
|
+
</Button>
|
|
267
|
+
)
|
|
268
|
+
const parcalar = c.split(/\s+/)
|
|
269
|
+
expect(parcalar).toContain('text-white')
|
|
270
|
+
expect(parcalar).toContain('bg-gradient-to-r')
|
|
271
|
+
expect(parcalar.filter((x) => x.startsWith('dark:text-'))).toEqual([])
|
|
272
|
+
// ⚠️ MEKANİZMANIN TAMAMI: twMerge `bg-gradient-to-r` ile `bg-primary`yi AYNI
|
|
273
|
+
// grupta saymaz → `bg-primary` sağ kalır (gradyan onu boyar, sorun değil).
|
|
274
|
+
// `text-white` ise `text-primary-foreground`u ELER. Geriye yalnız `dark:` ikizi
|
|
275
|
+
// kalıyordu ve koyu temada (0,2,0) > (0,1,0) ile kazanıyordu.
|
|
276
|
+
expect(parcalar).toContain('bg-primary')
|
|
277
|
+
expect(parcalar).not.toContain('text-primary-foreground')
|
|
278
|
+
|
|
279
|
+
// Kusurlu metin rengi = dark `--primary-foreground` (kaynaktan okundu).
|
|
280
|
+
const kusurluMetin = semanticRgb('primary-foreground', 'dark')
|
|
281
|
+
expect(hex(kusurluMetin)).toBe('#020205') // "neredeyse siyah" iddiasının kilidi
|
|
282
|
+
// Gradyanın İKİ ucunda da ölçüldü — tüketicinin istediği beyaz geçiyor,
|
|
283
|
+
// component'in dayattığı near-black KALIYOR:
|
|
284
|
+
const INDIGO_600: RGB = [79, 70, 229]
|
|
285
|
+
const PURPLE_600: RGB = [147, 51, 234]
|
|
286
|
+
for (const zemin of [INDIGO_600, PURPLE_600]) {
|
|
287
|
+
const beyaz = kontrast([255, 255, 255], zemin)
|
|
288
|
+
const kusurlu = kontrast(kusurluMetin, zemin)
|
|
289
|
+
expect(`${hex(zemin)} beyaz:${beyaz >= AA_TEXT ? 'geçer' : beyaz.toFixed(2)}`).toBe(
|
|
290
|
+
`${hex(zemin)} beyaz:geçer`
|
|
291
|
+
)
|
|
292
|
+
expect(kusurlu).toBeLessThan(AA_TEXT)
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('#715 Dal 1 — GENEL dedektör: aynı değerin `dark:` tekrarı kalmadı', () => {
|
|
298
|
+
/**
|
|
299
|
+
* Kural: `dark:X` yalnızca (a) öneksiz ikizinden FARKLI bir değere işaret ediyorsa,
|
|
300
|
+
* ya da (b) SIRALAMA GUARD'ı ise meşrudur.
|
|
301
|
+
*
|
|
302
|
+
* (b) şu durumda doğar: varyant, aynı CSS özelliği için FARKLI değerli bir
|
|
303
|
+
* `dark:` kuralı da bildiriyorsa (ör. `dark:bg-error-600`). Tailwind `dark:`i
|
|
304
|
+
* `hover:`ten SONRA yazdığı ve ikisi de (0,2,0) olduğu için, öneksiz
|
|
305
|
+
* `hover:bg-error-700` koyu temada `dark:bg-error-600`a KAYBEDER; onu yenmek
|
|
306
|
+
* (0,3,0) özgüllüğündeki `dark:hover:` ikizini ZORUNLU kılar.
|
|
307
|
+
*/
|
|
308
|
+
const ozellik = (c: string) => c.replace(/^(?:[a-z-]+:)+/, '').match(/^(bg|text|border|ring)-/)?.[1]
|
|
309
|
+
|
|
310
|
+
it.each(PAKETLER)('%s: her `dark:` sınıfının ya farklı değeri ya guard görevi var', (_ad, src) => {
|
|
311
|
+
const ihlaller: string[] = []
|
|
312
|
+
for (const v of VARYANTLAR) {
|
|
313
|
+
const liste = variantClasses(src, v)
|
|
314
|
+
// Bu varyant, hangi özellikler için FARKLI değerli bir `dark:` taban kuralı
|
|
315
|
+
// bildiriyor? (guard gerekçesi yalnız o özellikler için geçerli)
|
|
316
|
+
const guardOzellikleri = new Set(
|
|
317
|
+
liste
|
|
318
|
+
.filter((c) => /^dark:(?!hover:|active:|focus-visible:)/.test(c))
|
|
319
|
+
.filter((c) => !liste.includes(c.slice('dark:'.length)))
|
|
320
|
+
.map(ozellik)
|
|
321
|
+
.filter(Boolean) as string[]
|
|
322
|
+
)
|
|
323
|
+
for (const c of liste.filter((x) => x.startsWith('dark:'))) {
|
|
324
|
+
const oneksiz = c.slice('dark:'.length)
|
|
325
|
+
if (!liste.includes(oneksiz)) continue // farklı değer → meşru
|
|
326
|
+
if (guardOzellikleri.has(ozellik(c) as string)) continue // sıralama guard'ı → meşru
|
|
327
|
+
ihlaller.push(`${v}: ${c}`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
expect(ihlaller).toEqual([])
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it.each(PAKETLER)('%s: `destructive`in guard ikizleri KALDI (ters yönlü kilit)', (_ad, src) => {
|
|
334
|
+
const liste = variantClasses(src, 'destructive')
|
|
335
|
+
expect(liste).toContain('dark:bg-error-600')
|
|
336
|
+
// `dark:bg-error-600` durdukça bu ikisi kaldırılamaz — kaldırılırsa koyu temada
|
|
337
|
+
// hover/active resting rengine geri düşer.
|
|
338
|
+
expect(liste).toContain('dark:hover:bg-error-700')
|
|
339
|
+
expect(liste).toContain('dark:active:bg-error-800')
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
344
|
+
// Dal 2 — `destructive` durum-bağımlı dalları
|
|
345
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
346
|
+
|
|
347
|
+
describe('#715 Dal 2 — `destructive` hover/active kompoziti AA taşıyor', () => {
|
|
348
|
+
it.each(PAKETLER)('%s: KONTROL GRUBU — resting DEĞİŞMEDİ (#442)', (_ad, src) => {
|
|
349
|
+
expect(variantClass(src, 'destructive')).toContain('bg-destructive text-destructive-foreground')
|
|
350
|
+
const zemin = fill(src, 'destructive', 'light', WHITE)
|
|
351
|
+
const oran = kontrast(zemin, textColor(src, 'destructive', 'light', zemin))
|
|
352
|
+
expect(hex(zemin)).toBe('#e11313')
|
|
353
|
+
expect(oran).toBeCloseTo(4.66, 1)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it.each(PAKETLER)('%s: LIGHT hover + active ≥ 4.5 (kusurun kendisi)', (_ad, src) => {
|
|
357
|
+
for (const onek of ['hover:', 'active:']) {
|
|
358
|
+
const zemin = fill(src, 'destructive', 'light', WHITE, onek)
|
|
359
|
+
const oran = kontrast(zemin, textColor(src, 'destructive', 'light', zemin))
|
|
360
|
+
expect(`light ${onek}${hex(zemin)}: ${oran >= AA_TEXT ? 'geçer' : oran.toFixed(2)}`).toBe(
|
|
361
|
+
`light ${onek}${hex(zemin)}: geçer`
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it.each(PAKETLER)('%s: DARK hover + active ≥ 4.5 (regresyon açılmadı)', (_ad, src) => {
|
|
367
|
+
for (const onek of ['hover:', 'active:']) {
|
|
368
|
+
const zemin = fill(src, 'destructive', 'dark', CARD_ACTUAL_DARK, onek)
|
|
369
|
+
const oran = kontrast(zemin, textColor(src, 'destructive', 'dark', zemin))
|
|
370
|
+
expect(`dark ${onek}${hex(zemin)}: ${oran >= AA_TEXT ? 'geçer' : oran.toFixed(2)}`).toBe(
|
|
371
|
+
`dark ${onek}${hex(zemin)}: geçer`
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it.each(PAKETLER)('%s: alfa harmanı KALMADI — kontrast artık zemine bağımlı değil', (_ad, src) => {
|
|
377
|
+
const alfali = variantClasses(src, 'destructive').filter((c) => /bg-[a-z-]+(?:-\d+)?\/\d+/.test(c))
|
|
378
|
+
expect(alfali).toEqual([])
|
|
379
|
+
expect(variantClasses(src, 'destructive')).toEqual(
|
|
380
|
+
expect.arrayContaining(['hover:bg-error-700', 'active:bg-error-800'])
|
|
381
|
+
)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it.each(PAKETLER)('%s: durum ilerlemesi tutarlı KOYULAŞIYOR', (_ad, src) => {
|
|
385
|
+
// Alfa sürümünde active (#e74242) hover'dan (#e42b2b) DAHA AÇIKtı — hem a11y
|
|
386
|
+
// hem etkileşim geri bildirimi bakımından ters.
|
|
387
|
+
const l = (onek: string) => relativeLuminance(fill(src, 'destructive', 'light', WHITE, onek))
|
|
388
|
+
expect(l('')).toBeGreaterThan(l('hover:'))
|
|
389
|
+
expect(l('hover:')).toBeGreaterThan(l('active:'))
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('render: `destructive` çıktısı yeni hover sınıfını taşıyor, restingi korumuş', () => {
|
|
393
|
+
const c = cls(<Button variant="destructive">x</Button>).split(/\s+/)
|
|
394
|
+
expect(c).toContain('bg-destructive')
|
|
395
|
+
expect(c).toContain('text-destructive-foreground')
|
|
396
|
+
expect(c).toContain('hover:bg-error-700')
|
|
397
|
+
expect(c).not.toContain('hover:bg-destructive/90')
|
|
398
|
+
expect(c).not.toContain('active:bg-destructive/80')
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
403
|
+
// Dal 3 — `primary` + `success` durum-bağımlı dalları
|
|
404
|
+
//
|
|
405
|
+
// Dal 2 yalnız `destructive`i onarmıştı; AYNI alfa kusuru iki varyantta daha
|
|
406
|
+
// ölçüldü (metin her ikisinde de sabit `…-foreground` = #f8fafc, zemin beyaz):
|
|
407
|
+
// primary hover bg-primary/90 #2876f5 → 4.01 ✗ | active /80 #4085f6 → 3.40 ✗
|
|
408
|
+
// success hover bg-success/90 #2a8f4f → 3.90 ✗ | active /80 #419b62 → 3.30 ✗
|
|
409
|
+
// İki varyant AYNI kusuru taşısa da AYNI çözümü ALMADI, çünkü skala aileleri
|
|
410
|
+
// farklı: `--success-NNN` `--success` ile aynı hue'da (opak kademe yeterli),
|
|
411
|
+
// `--primary-NNN` ise SKY paleti (`--primary` BLUE) ve preset onları hiçbir
|
|
412
|
+
// utility'ye bağlamıyor → `bg-primary-700` ölü sınıf olurdu. `primary` bu yüzden
|
|
413
|
+
// koyu tonu `color-mix` ile TOKEN'IN KENDİSİNDEN türetiyor.
|
|
414
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
415
|
+
|
|
416
|
+
describe('#715 Dal 3 — `primary` hover/active kompoziti AA taşıyor', () => {
|
|
417
|
+
it.each(PAKETLER)('%s: KONTROL GRUBU — resting DEĞİŞMEDİ (#442)', (_ad, src) => {
|
|
418
|
+
expect(variantClass(src, 'primary')).toContain('bg-primary text-primary-foreground')
|
|
419
|
+
const zemin = fill(src, 'primary', 'light', WHITE)
|
|
420
|
+
expect(hex(zemin)).toBe('#1067f4')
|
|
421
|
+
expect(kontrast(zemin, textColor(src, 'primary', 'light', zemin))).toBeCloseTo(4.7, 1)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it.each(PAKETLER)('%s: LIGHT hover + active ≥ 4.5 (kusurun kendisi)', (_ad, src) => {
|
|
425
|
+
for (const onek of ['hover:', 'active:']) {
|
|
426
|
+
const zemin = fill(src, 'primary', 'light', WHITE, onek)
|
|
427
|
+
const oran = kontrast(zemin, textColor(src, 'primary', 'light', zemin))
|
|
428
|
+
expect(`light ${onek}${hex(zemin)}: ${oran >= AA_TEXT ? 'geçer' : oran.toFixed(2)}`).toBe(
|
|
429
|
+
`light ${onek}${hex(zemin)}: geçer`
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it.each(PAKETLER)('%s: DARK hover + active ≥ 4.5 (regresyon açılmadı)', (_ad, src) => {
|
|
435
|
+
// Koyu temada `--primary` near-white, `--primary-foreground` near-black —
|
|
436
|
+
// yani koyulaştırma TERS yönde çalışabilirdi. Reddedilen sky-kademesi adayı
|
|
437
|
+
// tam burada 3.49'a çöküyordu; `color-mix` türevi çökmüyor.
|
|
438
|
+
for (const onek of ['hover:', 'active:']) {
|
|
439
|
+
const zemin = fill(src, 'primary', 'dark', CARD_ACTUAL_DARK, onek)
|
|
440
|
+
const oran = kontrast(zemin, textColor(src, 'primary', 'dark', zemin))
|
|
441
|
+
expect(`dark ${onek}${hex(zemin)}: ${oran >= AA_TEXT ? 'geçer' : oran.toFixed(2)}`).toBe(
|
|
442
|
+
`dark ${onek}${hex(zemin)}: geçer`
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it.each(PAKETLER)('%s: alfa harmanı KALMADI — kontrast zemine bağımlı değil', (_ad, src) => {
|
|
448
|
+
expect(variantClasses(src, 'primary').filter((c) => /bg-[a-z-]+(?:-\d+)?\/\d+/.test(c))).toEqual([])
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it.each(PAKETLER)('%s: koyu ton `--primary`nin KENDİSİNDEN türetiliyor', (_ad, src) => {
|
|
452
|
+
// Bu testin ASIL işi: sabit bir renge (`bg-blue-700`) kaçışı yasaklamak.
|
|
453
|
+
// Tüketici `--primary`yi override ettiğinde hover/active KENDİLİĞİNDEN uymalı.
|
|
454
|
+
for (const onek of ['hover:', 'active:']) {
|
|
455
|
+
const secili = variantClasses(src, 'primary').find((c) => c.startsWith(`${onek}bg-`))!
|
|
456
|
+
const m = secili.slice(onek.length).match(COLOR_MIX_RE)
|
|
457
|
+
expect(`${onek}${m ? m[1] : 'TÜREV DEĞİL: ' + secili}`).toBe(`${onek}primary`)
|
|
458
|
+
// `[color:…]` etiketi: sınıfın RENK olarak tanınmasını Tailwind'in ve
|
|
459
|
+
// twMerge'ün tip ÇIKARIMINA bırakmıyoruz (ikisi de bugün doğru çıkarım
|
|
460
|
+
// yapıyor — kilit, gelecekte sessizce değişmesine karşı).
|
|
461
|
+
expect(secili).toContain('bg-[color:')
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it.each(PAKETLER)('%s: durum ilerlemesi İKİ TEMADA da koyulaşıyor', (_ad, src) => {
|
|
466
|
+
for (const [blok, zemin] of [['light', WHITE], ['dark', CARD_ACTUAL_DARK]] as const) {
|
|
467
|
+
const l = (onek: string) => relativeLuminance(fill(src, 'primary', blok, zemin, onek))
|
|
468
|
+
expect(`${blok} resting>hover: ${l('') > l('hover:')}`).toBe(`${blok} resting>hover: true`)
|
|
469
|
+
expect(`${blok} hover>active: ${l('hover:') > l('active:')}`).toBe(`${blok} hover>active: true`)
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('render: `primary` çıktısı yeni dalları taşıyor, restingi korumuş', () => {
|
|
474
|
+
const c = cls(<Button variant="primary">x</Button>).split(/\s+/)
|
|
475
|
+
expect(c).toContain('bg-primary')
|
|
476
|
+
expect(c).toContain('text-primary-foreground')
|
|
477
|
+
expect(c).toContain('hover:bg-[color:color-mix(in_srgb,hsl(var(--primary))_85%,black)]')
|
|
478
|
+
expect(c).not.toContain('hover:bg-primary/90')
|
|
479
|
+
expect(c).not.toContain('active:bg-primary/80')
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('tüketici override\'ı hâlâ KAZANIYOR — arbitrary değer twMerge grubunu bozmadı', () => {
|
|
483
|
+
// #715'in tamamı "component sınıfı tüketiciyi eziyor" hikâyesi. Arbitrary
|
|
484
|
+
// değer yanlış GRUBA düşerse (ör. bg-image) twMerge onu elemez ve kusur
|
|
485
|
+
// yeni bir kılıkta geri gelir.
|
|
486
|
+
const c = cls(<Button className="hover:bg-red-500 active:bg-red-700">x</Button>).split(/\s+/)
|
|
487
|
+
expect(c).toContain('hover:bg-red-500')
|
|
488
|
+
expect(c).toContain('active:bg-red-700')
|
|
489
|
+
expect(c.filter((x) => x.includes('color-mix'))).toEqual([])
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
describe('#715 Dal 3 — `success` hover/active kompoziti AA taşıyor', () => {
|
|
494
|
+
it.each(PAKETLER)('%s: KONTROL GRUBU — resting DEĞİŞMEDİ (#442)', (_ad, src) => {
|
|
495
|
+
expect(variantClass(src, 'success')).toContain('bg-success text-success-foreground')
|
|
496
|
+
const zemin = fill(src, 'success', 'light', WHITE)
|
|
497
|
+
expect(hex(zemin)).toBe('#12823b')
|
|
498
|
+
expect(kontrast(zemin, textColor(src, 'success', 'light', zemin))).toBeCloseTo(4.69, 1)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it.each(PAKETLER)('%s: LIGHT hover + active ≥ 4.5 (kusurun kendisi)', (_ad, src) => {
|
|
502
|
+
for (const onek of ['hover:', 'active:']) {
|
|
503
|
+
const zemin = fill(src, 'success', 'light', WHITE, onek)
|
|
504
|
+
const oran = kontrast(zemin, textColor(src, 'success', 'light', zemin))
|
|
505
|
+
expect(`light ${onek}${hex(zemin)}: ${oran >= AA_TEXT ? 'geçer' : oran.toFixed(2)}`).toBe(
|
|
506
|
+
`light ${onek}${hex(zemin)}: geçer`
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it.each(PAKETLER)('%s: DARK hover + active ≥ 4.5 (regresyon açılmadı)', (_ad, src) => {
|
|
512
|
+
for (const onek of ['hover:', 'active:']) {
|
|
513
|
+
const zemin = fill(src, 'success', 'dark', CARD_ACTUAL_DARK, onek)
|
|
514
|
+
const oran = kontrast(zemin, textColor(src, 'success', 'dark', zemin))
|
|
515
|
+
expect(`dark ${onek}${hex(zemin)}: ${oran >= AA_TEXT ? 'geçer' : oran.toFixed(2)}`).toBe(
|
|
516
|
+
`dark ${onek}${hex(zemin)}: geçer`
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it.each(PAKETLER)('%s: alfa harmanı KALMADI, opak kademelere bağlandı', (_ad, src) => {
|
|
522
|
+
expect(variantClasses(src, 'success').filter((c) => /bg-[a-z-]+(?:-\d+)?\/\d+/.test(c))).toEqual([])
|
|
523
|
+
expect(variantClasses(src, 'success')).toEqual(
|
|
524
|
+
expect.arrayContaining(['hover:bg-success-800', 'active:bg-success-900'])
|
|
525
|
+
)
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it.each(PAKETLER)('%s: -700 SEÇİLMEDİ — restingden ayırt edilemezdi', (_ad, src) => {
|
|
529
|
+
// `bg-success-700` (#15803d) 4.79 ile eşiği geçiyor AMA `bg-success` (#12823b)
|
|
530
|
+
// ile arasında pratikte fark yok; hover geri bildirimi görünmez olurdu.
|
|
531
|
+
// ΔL (bağıl parlaklık) eşiği: gözle seçilebilir bir durum değişimi için
|
|
532
|
+
// resting'in en az %15'i kadar düşüş istiyoruz.
|
|
533
|
+
const resting = relativeLuminance(fill(src, 'success', 'light', WHITE))
|
|
534
|
+
const hover = relativeLuminance(fill(src, 'success', 'light', WHITE, 'hover:'))
|
|
535
|
+
const yediYuz = relativeLuminance(scaleRgb('success-700'))
|
|
536
|
+
expect(`-700 farkı yeterli mi: ${(resting - yediYuz) / resting > 0.15}`).toBe(
|
|
537
|
+
'-700 farkı yeterli mi: false'
|
|
538
|
+
)
|
|
539
|
+
expect(`seçilen fark yeterli mi: ${(resting - hover) / resting > 0.15}`).toBe(
|
|
540
|
+
'seçilen fark yeterli mi: true'
|
|
541
|
+
)
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it.each(PAKETLER)('%s: `dark:` guard ikizleri KALDI (ters yönlü kilit)', (_ad, src) => {
|
|
545
|
+
const liste = variantClasses(src, 'success')
|
|
546
|
+
// `dark:bg-success-700` FARKLI değerde bir dark taban kuralı; Tailwind onu
|
|
547
|
+
// `hover:`ten SONRA yazıyor ve ikisi de (0,2,0) → önek atılırsa koyu temada
|
|
548
|
+
// hover resting rengine geri düşer. Bu iki satır o yüzden guard.
|
|
549
|
+
expect(liste).toContain('dark:bg-success-700')
|
|
550
|
+
expect(liste).toContain('dark:hover:bg-success-800')
|
|
551
|
+
expect(liste).toContain('dark:active:bg-success-900')
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it.each(PAKETLER)('%s: durum ilerlemesi İKİ TEMADA da koyulaşıyor', (_ad, src) => {
|
|
555
|
+
for (const [blok, zemin] of [['light', WHITE], ['dark', CARD_ACTUAL_DARK]] as const) {
|
|
556
|
+
const l = (onek: string) => relativeLuminance(fill(src, 'success', blok, zemin, onek))
|
|
557
|
+
expect(`${blok} resting>hover: ${l('') > l('hover:')}`).toBe(`${blok} resting>hover: true`)
|
|
558
|
+
expect(`${blok} hover>active: ${l('hover:') > l('active:')}`).toBe(`${blok} hover>active: true`)
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('render: `success` çıktısı yeni dalları taşıyor, restingi korumuş', () => {
|
|
563
|
+
const c = cls(<Button variant="success">x</Button>).split(/\s+/)
|
|
564
|
+
expect(c).toContain('bg-success')
|
|
565
|
+
expect(c).toContain('hover:bg-success-800')
|
|
566
|
+
expect(c).not.toContain('hover:bg-success/90')
|
|
567
|
+
expect(c).not.toContain('active:bg-success/80')
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
572
|
+
// ÖLÜ SINIF DEDEKTÖRÜ (#713 sınıfı) — Dal 3'ün asıl dersi
|
|
573
|
+
//
|
|
574
|
+
// `bg-primary-700` "çalışıyor gibi" duruyor: `--primary-700` tokens.css'te TANIMLI.
|
|
575
|
+
// Ama preset onu hiçbir utility'ye bağlamadığı için Tailwind o sınıfı HİÇ ÜRETMEZ
|
|
576
|
+
// → stil sessizce kaybolur. Dal 3'te bu tuzağa düşülmedi; aşağıdaki kilit bir
|
|
577
|
+
// sonraki geliştiricinin düşmesini de engeller.
|
|
578
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
579
|
+
|
|
580
|
+
describe('#715 — button, preset\'in ÜRETMEDİĞİ hiçbir token sınıfı kullanmıyor', () => {
|
|
581
|
+
// Preset düz CJS; tailwindcss'e bağımlı değil, doğrudan require edilebilir.
|
|
582
|
+
const preset = require(path.join(PKG, '../tailwind-preset.js'))
|
|
583
|
+
const presetColors: Record<string, unknown> = preset.theme.extend.colors
|
|
584
|
+
|
|
585
|
+
it.each(PAKETLER)('%s: kullanılan her kademe preset\'te tanımlı', (_ad, src) => {
|
|
586
|
+
const olu: string[] = []
|
|
587
|
+
for (const v of VARYANTLAR) {
|
|
588
|
+
for (const c of variantClasses(src, v)) {
|
|
589
|
+
const g = c.replace(/^(?:(?:dark|hover|active|focus-visible|group-hover):)+/, '')
|
|
590
|
+
const m = g.match(/^(?:bg|text|border|ring)-([a-z-]+?)-(\d{2,3})(?:\/\d{1,3})?$/)
|
|
591
|
+
if (!m) continue
|
|
592
|
+
const [, aile, kademe] = m
|
|
593
|
+
// Tailwind'in KENDİ paleti (gray/blue/green/purple/pink…) preset'te yok
|
|
594
|
+
// ve her zaman üretilir → yalnız token-destekli aileleri denetle.
|
|
595
|
+
if (!(aile in presetColors)) continue
|
|
596
|
+
if (!(kademe in (presetColors[aile] as Record<string, unknown>))) olu.push(`${v}: ${c}`)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
expect(olu).toEqual([])
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('KONTROL: dedektör gerçekten yakalıyor — `bg-primary-700` ölü sınıftır', () => {
|
|
603
|
+
// Reddedilen adayın kilidi. `--primary-700` tokens.css'te VAR…
|
|
604
|
+
expect(tokenOku('primary-700', 'light')).toBe('3 105 161')
|
|
605
|
+
// …ama preset'te YOK → `bg-primary-700` hiç CSS üretmez.
|
|
606
|
+
expect('700' in (presetColors.primary as Record<string, unknown>)).toBe(false)
|
|
607
|
+
// Üstelik yanlış hue: `--primary` blue, `--primary-700` sky.
|
|
608
|
+
expect(hex(semanticRgb('primary', 'light'))).toBe('#1067f4')
|
|
609
|
+
expect(hex(scaleRgb('primary-700'))).toBe('#0369a1')
|
|
610
|
+
})
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
614
|
+
// İkiz pariteği — #643'ün "tek pakete yazma" tuzağına karşı kalıcı kilit
|
|
615
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
616
|
+
|
|
617
|
+
describe('#715 — free ↔ pro ikiz pariteği (12 varyantın tamamı)', () => {
|
|
618
|
+
it.each(VARYANTLAR)('`%s` varyantı iki pakette AYNI sınıf kümesi', (v) => {
|
|
619
|
+
const norm = (src: string) => [...new Set(variantClasses(src, v))].sort()
|
|
620
|
+
expect({ varyant: v, free: norm(freeSrc) }).toEqual({ varyant: v, free: norm(proSrc) })
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('varyant ADLARI da aynı — birinde olup diğerinde olmayan yok', () => {
|
|
624
|
+
const adlar = (src: string) => {
|
|
625
|
+
const blok = src.slice(src.indexOf('variant: {'))
|
|
626
|
+
return [...blok.matchAll(/^ {8}"?([a-zA-Z-]+)"?:\s*\[/gm)].map((m) => m[1])
|
|
627
|
+
}
|
|
628
|
+
expect(adlar(freeSrc)).toEqual(adlar(proSrc))
|
|
629
|
+
expect(adlar(freeSrc).sort()).toEqual([...VARYANTLAR].sort())
|
|
630
|
+
})
|
|
631
|
+
})
|
|
@@ -396,11 +396,21 @@ describe('Select Components', () => {
|
|
|
396
396
|
// #623: `text-warning` (DEFAULT token) açık temada beyaz zeminde 2.14:1 ölçüldü —
|
|
397
397
|
// AA'nın (4.5) ve hatta büyük-metin eşiğinin (3.0) altında. `--warning` aynı zamanda
|
|
398
398
|
// `bg-warning` yüzeylerinde kullanıldığı için token'ın kendisi DEĞİŞTİRİLMEDİ;
|
|
399
|
-
// metin rengi
|
|
399
|
+
// metin rengi ham kademe ikilisine taşınmıştı: 4.92 (light) / 10.51 (dark).
|
|
400
|
+
//
|
|
401
|
+
// Kalite koşusu 12 (#710 sınıfı): kardeş varyantlar `destructive` ve `success`
|
|
402
|
+
// `-subtle` token ailesine geçirilmişti, `warning` bu göçün DIŞINDA kalmıştı.
|
|
403
|
+
// Artık hizalandı. İki kazanım:
|
|
404
|
+
// 1. Kontrast 4.92 → 8.83 (light) / 10.51 → 14.43 (dark) — tokens.css'te yazılı
|
|
405
|
+
// 2. `dark:` override KALKTI: token zaten tema duyarlı olduğu için gereksizdi,
|
|
406
|
+
// üstelik `dark:` sınıfı tüketicinin kendi rengini eziyordu (#715'in kök nedeni)
|
|
407
|
+
// Aşağıdaki `not.toHaveClass` bilinçli bir regresyon kilidi — ham kademeye
|
|
408
|
+
// ya da `dark:` override'ına geri dönüş bu testi kırar.
|
|
400
409
|
rerender(<SelectItem value="item1" variant="warning">Item</SelectItem>)
|
|
401
410
|
item = screen.getByTestId('select-item')
|
|
402
|
-
expect(item).toHaveClass('text-warning-
|
|
403
|
-
expect(item).toHaveClass('
|
|
411
|
+
expect(item).toHaveClass('text-warning-subtle-foreground')
|
|
412
|
+
expect(item).not.toHaveClass('text-warning-700')
|
|
413
|
+
expect(item).not.toHaveClass('dark:text-warning-500')
|
|
404
414
|
})
|
|
405
415
|
|
|
406
416
|
it('applies size classes correctly', () => {
|