@moontra/moonui 6.24.0 → 6.26.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 +12 -12
- package/dist/cdn/moonui.global.js +12 -12
- package/dist/index.d.mts +24 -6
- package/dist/index.d.ts +24 -6
- package/dist/index.js +436 -93
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +437 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/issue-544-kontrast.test.tsx +153 -145
- package/src/__tests__/issue-645-overlay-tab-focus.test.tsx +262 -0
- package/src/__tests__/issue-646-dark-scope-override.test.tsx +297 -0
- package/src/__tests__/issue-661-a11y.test.tsx +113 -0
- package/src/__tests__/issue-661-token-kontrast.test.tsx +82 -0
- package/src/components/ui/__tests__/accent-foreground-contrast.test.ts +197 -0
- package/src/components/ui/__tests__/accordion.test.tsx +6 -4
- package/src/components/ui/__tests__/alert.test.tsx +4 -1
- package/src/components/ui/__tests__/button.test.tsx +6 -1
- package/src/components/ui/__tests__/dark-token-contrast.test.ts +18 -18
- package/src/components/ui/__tests__/heading-level-policy.test.tsx +4 -4
- package/src/components/ui/__tests__/issue-643-card-token-parite.test.tsx +113 -0
- package/src/components/ui/__tests__/issue-649-button-aschild.test.tsx +199 -0
- package/src/components/ui/__tests__/issue-664-separator-dekoratif-rol.test.tsx +41 -0
- package/src/components/ui/__tests__/preset-color-shadowing.test.ts +10 -10
- package/src/components/ui/__tests__/primary-subtle-token.test.ts +7 -7
- package/src/components/ui/__tests__/progress-secondary-contrast.test.ts +38 -38
- package/src/components/ui/__tests__/progress-warning-error-contrast.test.ts +64 -64
- package/src/components/ui/__tests__/quality-group-de.test.tsx +2 -2
- package/src/components/ui/__tests__/responsive-color-invariance.test.ts +20 -20
- package/src/components/ui/__tests__/select.test.tsx +6 -1
- package/src/components/ui/__tests__/semantic-token-usage.test.ts +23 -23
- package/src/components/ui/__tests__/size-scale-convention.test.ts +10 -10
- package/src/components/ui/__tests__/slider-accent-contrast.test.ts +142 -136
- package/src/components/ui/__tests__/slider-secondary-contrast.test.ts +60 -60
- package/src/components/ui/__tests__/slider-warning-error-contrast.test.ts +100 -100
- package/src/components/ui/__tests__/switch-variant-contrast.test.tsx +103 -103
- package/src/components/ui/__tests__/tooltip.test.tsx +122 -0
- package/src/components/ui/accordion.tsx +16 -3
- package/src/components/ui/alert.tsx +1 -1
- package/src/components/ui/badge.tsx +12 -1
- package/src/components/ui/button.tsx +95 -24
- package/src/components/ui/card.tsx +59 -5
- package/src/components/ui/dropdown-menu.tsx +99 -5
- package/src/components/ui/file-upload.tsx +4 -0
- package/src/components/ui/index.ts +4 -1
- package/src/components/ui/popover-pro.tsx +41 -0
- package/src/components/ui/popover.tsx +162 -7
- package/src/components/ui/rating.tsx +24 -2
- package/src/components/ui/select.tsx +4 -4
- package/src/components/ui/simple-editor.tsx +3 -18
- package/src/components/ui/tabs.tsx +16 -1
- package/src/components/ui/textarea.tsx +6 -1
- package/src/components/ui/toast.tsx +17 -2
- package/src/components/ui/tooltip.tsx +154 -2
- package/src/styles/tokens.css +34 -2
|
@@ -53,12 +53,12 @@ type RGB = [number, number, number]
|
|
|
53
53
|
type Blok = 'light' | 'dark'
|
|
54
54
|
|
|
55
55
|
/** "H S% L%" ya da "R G B" biçimindeki ham token değeri (light = `.moonui-root`, dark = `.dark`) */
|
|
56
|
-
function tokenOku(
|
|
57
|
-
const
|
|
56
|
+
function tokenOku(name: string, blok: Blok): string | null {
|
|
57
|
+
const source =
|
|
58
58
|
blok === 'dark'
|
|
59
59
|
? tokens.slice(tokens.indexOf('.dark'))
|
|
60
60
|
: tokens.slice(0, tokens.indexOf('.dark'))
|
|
61
|
-
return
|
|
61
|
+
return source.match(new RegExp(`--${name}:\\s*([^;]+);`))?.[1].trim() ?? null
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
@@ -85,10 +85,10 @@ const rgbTokenRgb = (v: string): RGB => {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/** Semantik token'ın ilgili temadaki değeri — `.dark` ezmesi YOKSA light'a düşer */
|
|
88
|
-
const
|
|
89
|
-
hslTokenRgb(tokenOku(
|
|
88
|
+
const semanticRgb = (name: string, blok: Blok): RGB =>
|
|
89
|
+
hslTokenRgb(tokenOku(name, blok) ?? tokenOku(name, 'light')!)
|
|
90
90
|
|
|
91
|
-
function
|
|
91
|
+
function relativeLuminance([r, g, b]: RGB): number {
|
|
92
92
|
const c = (v: number) => {
|
|
93
93
|
const x = v / 255
|
|
94
94
|
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
|
|
@@ -97,8 +97,8 @@ function bagilParlaklik([r, g, b]: RGB): number {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function kontrast(a: RGB, b: RGB): number {
|
|
100
|
-
const la =
|
|
101
|
-
const lb =
|
|
100
|
+
const la = relativeLuminance(a)
|
|
101
|
+
const lb = relativeLuminance(b)
|
|
102
102
|
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
103
103
|
return (hi + 0.05) / (lo + 0.05)
|
|
104
104
|
}
|
|
@@ -130,61 +130,61 @@ function deltaE(a: RGB, b: RGB): number {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/** Docs kabuğunda ölçülen GERÇEK koyu kart zemini (#284/#368/#448 konvansiyonu) */
|
|
133
|
-
const
|
|
133
|
+
const CARD_ACTUAL_DARK: RGB = [17, 24, 39]
|
|
134
134
|
|
|
135
|
-
function
|
|
135
|
+
function backgrounds(blok: Blok): Array<[string, RGB]> {
|
|
136
136
|
const z: Array<[string, RGB]> = [
|
|
137
|
-
['--background',
|
|
138
|
-
['--card',
|
|
137
|
+
['--background', semanticRgb('background', blok)],
|
|
138
|
+
['--card', semanticRgb('card', blok)],
|
|
139
139
|
]
|
|
140
|
-
if (blok === 'dark') z.push(['gerçek kart #111827',
|
|
140
|
+
if (blok === 'dark') z.push(['gerçek kart #111827', CARD_ACTUAL_DARK])
|
|
141
141
|
return z
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// ——— Renkler KAYNAKTAN türetilir (PR #440/#446/#453 dersi) ——————————————
|
|
145
|
-
const
|
|
145
|
+
const source = fs.readFileSync(path.join(KOK, 'components/ui/switch.tsx'), 'utf8')
|
|
146
146
|
/** Yorum satırları ayıklanır — gerekçe metinleri `varyant:` gibi görünmesin */
|
|
147
|
-
const
|
|
148
|
-
const TRACK_CVA =
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
const stripComments = source.replace(/^[ \t]*\/\/.*$/gm, '')
|
|
148
|
+
const TRACK_CVA = stripComments.slice(
|
|
149
|
+
stripComments.indexOf('const switchVariants'),
|
|
150
|
+
stripComments.indexOf('const switchThumbVariants')
|
|
151
151
|
)
|
|
152
|
-
const THUMB_CVA =
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const THUMB_CVA = stripComments.slice(
|
|
153
|
+
stripComments.indexOf('const switchThumbVariants'),
|
|
154
|
+
stripComments.indexOf('export interface SwitchProps')
|
|
155
155
|
)
|
|
156
156
|
|
|
157
|
-
const
|
|
158
|
-
type Varyant = (typeof
|
|
157
|
+
const VARIANTS = ['primary', 'success', 'warning', 'destructive', 'secondary'] as const
|
|
158
|
+
type Varyant = (typeof VARIANTS)[number]
|
|
159
159
|
|
|
160
160
|
/** CVA `variant` bloğundaki ham sınıf dizgesi */
|
|
161
|
-
function
|
|
162
|
-
const m = TRACK_CVA.match(new RegExp(`\\n\\s*${
|
|
163
|
-
if (!m) throw new Error(`${
|
|
161
|
+
function variantClass(variantVal: string): string {
|
|
162
|
+
const m = TRACK_CVA.match(new RegExp(`\\n\\s*${variantVal}:\\s*"([^"]+)"`))
|
|
163
|
+
if (!m) throw new Error(`${variantVal} varyantı switchVariants içinde bulunamadı`)
|
|
164
164
|
return m[1]
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/** `bg-primary` / `bg-[rgb(var(--secondary-500))]` → RGB */
|
|
168
|
-
function
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
throw new Error(`çözülemeyen sınıf: ${
|
|
168
|
+
function classColor(cls: string, blok: Blok): RGB {
|
|
169
|
+
const scale = cls.match(/^bg-\[rgb\(var\(--([a-z0-9-]+)\)\)\]$/)
|
|
170
|
+
if (scale) return rgbTokenRgb(tokenOku(scale[1], 'light')!) // skala YALNIZ light blokta
|
|
171
|
+
const semantic = cls.match(/^bg-([a-z-]+)$/)
|
|
172
|
+
if (semantic) return semanticRgb(semantic[1], blok)
|
|
173
|
+
throw new Error(`çözülemeyen sınıf: ${cls}`)
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/** checked track'in ilgili temadaki rengi — `dark:` önekli sınıf dark'ta kazanır */
|
|
177
|
-
function checkedTrackRengi(
|
|
177
|
+
function checkedTrackRengi(variantVal: string, blok: Blok): RGB {
|
|
178
178
|
const KOSUL = 'data-[state=checked]:'
|
|
179
|
-
const adaylar =
|
|
179
|
+
const adaylar = variantClass(variantVal)
|
|
180
180
|
.split(/\s+/)
|
|
181
181
|
.filter(Boolean)
|
|
182
182
|
.filter((s) => s.replace(/^dark:/, '').startsWith(KOSUL))
|
|
183
183
|
const soy = (s: string) => s.replace(/^dark:/, '').slice(KOSUL.length)
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
187
|
-
return
|
|
184
|
+
const darkClass = adaylar.find((s) => s.startsWith('dark:'))
|
|
185
|
+
const lightClass = adaylar.find((s) => !s.startsWith('dark:'))
|
|
186
|
+
if (!lightClass) throw new Error(`${variantVal}: temel (light) checked sınıfı yok`)
|
|
187
|
+
return classColor(soy(blok === 'dark' && darkClass ? darkClass : lightClass), blok)
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/** Thumb'ın dolgusu — A ve B eksenlerinin AYNI sayıyı vermesinin sebebi */
|
|
@@ -192,24 +192,24 @@ function thumbRengi(blok: Blok): RGB {
|
|
|
192
192
|
const m = THUMB_CVA.match(/"([^"]*\bbg-[a-z-]+\b[^"]*)"/)
|
|
193
193
|
if (!m) throw new Error('thumb taban sınıfı okunamadı')
|
|
194
194
|
const bg = m[1].split(/\s+/).find((s) => s.startsWith('bg-'))!
|
|
195
|
-
return
|
|
195
|
+
return classColor(bg, blok)
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
const KADEMELER = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
199
|
-
const
|
|
200
|
-
const
|
|
199
|
+
const THRESHOLD = 3.0
|
|
200
|
+
const DELTAE_THRESHOLD = 10
|
|
201
201
|
|
|
202
202
|
/** Bir rengin switch eksenlerinde (A + B) o temada eşiği geçip geçmediği */
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
kontrast(thumbRengi(blok), c) >=
|
|
203
|
+
const passesAxes = (c: RGB, blok: Blok): boolean =>
|
|
204
|
+
backgrounds(blok).every(([, z]) => kontrast(c, z) >= THRESHOLD) &&
|
|
205
|
+
kontrast(thumbRengi(blok), c) >= THRESHOLD
|
|
206
206
|
|
|
207
207
|
describe('#454 — hesaplayıcı çapaları', () => {
|
|
208
208
|
it('kontrast hesaplayıcısı DOĞRU — belgelenmiş token çifti beklenen oranı veriyor', () => {
|
|
209
209
|
// tokens.css `--info-subtle` / `--info-subtle-foreground` için 9.5:1 iddia ediyor
|
|
210
210
|
const oran = kontrast(
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
semanticRgb('info-subtle', 'light'),
|
|
212
|
+
semanticRgb('info-subtle-foreground', 'light')
|
|
213
213
|
)
|
|
214
214
|
expect(oran).toBeGreaterThan(9.3)
|
|
215
215
|
expect(oran).toBeLessThan(9.7)
|
|
@@ -222,13 +222,13 @@ describe('#454 — hesaplayıcı çapaları', () => {
|
|
|
222
222
|
|
|
223
223
|
it('hesaplayıcı, issue #454 gövdesindeki ESKİ ölçümleri birebir üretiyor', () => {
|
|
224
224
|
// 1.154 / 1.359 / 1.197 — `bg-accent` ile checked track ↔ üç zemin
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
Number(kontrast(
|
|
225
|
+
const old = (blok: Blok) =>
|
|
226
|
+
backgrounds(blok).map(([, z]) =>
|
|
227
|
+
Number(kontrast(semanticRgb('accent', blok), z).toFixed(3))
|
|
228
228
|
)
|
|
229
|
-
expect(
|
|
230
|
-
expect(
|
|
231
|
-
expect(
|
|
229
|
+
expect(old('light')[0]).toBe(1.154)
|
|
230
|
+
expect(old('dark')[0]).toBe(1.359)
|
|
231
|
+
expect(old('dark')[2]).toBe(1.197)
|
|
232
232
|
})
|
|
233
233
|
})
|
|
234
234
|
|
|
@@ -238,7 +238,7 @@ describe('#454 — iki eksenin AYNI olmasının yapısal sebebi', () => {
|
|
|
238
238
|
// "tek ölçüm iki ekseni kapatır" varsayımı sessizce geçersizleşir.
|
|
239
239
|
expect(THUMB_CVA).toContain('bg-background')
|
|
240
240
|
for (const blok of ['light', 'dark'] as const) {
|
|
241
|
-
expect(thumbRengi(blok)).toEqual(
|
|
241
|
+
expect(thumbRengi(blok)).toEqual(semanticRgb('background', blok))
|
|
242
242
|
}
|
|
243
243
|
})
|
|
244
244
|
|
|
@@ -252,17 +252,17 @@ describe('#454 — iki eksenin AYNI olmasının yapısal sebebi', () => {
|
|
|
252
252
|
})
|
|
253
253
|
|
|
254
254
|
describe('#454 — KABUL KRİTERİ: her varyantın checked track’i iki eksende ≥3:1', () => {
|
|
255
|
-
for (const
|
|
255
|
+
for (const variantVal of VARIANTS) {
|
|
256
256
|
for (const blok of ['light', 'dark'] as const) {
|
|
257
|
-
it(`${
|
|
258
|
-
const c = checkedTrackRengi(
|
|
259
|
-
for (const [
|
|
257
|
+
it(`${variantVal} / ${blok}`, () => {
|
|
258
|
+
const c = checkedTrackRengi(variantVal, blok)
|
|
259
|
+
for (const [name, z] of backgrounds(blok)) {
|
|
260
260
|
// Zemin adı hata mesajına girsin diye ayrı assertion
|
|
261
|
-
expect(`${
|
|
262
|
-
`${
|
|
261
|
+
expect(`${name}: ${kontrast(c, z) >= THRESHOLD ? 'geçer' : kontrast(c, z).toFixed(3)}`).toBe(
|
|
262
|
+
`${name}: geçer`
|
|
263
263
|
)
|
|
264
264
|
}
|
|
265
|
-
expect(kontrast(thumbRengi(blok), c)).toBeGreaterThanOrEqual(
|
|
265
|
+
expect(kontrast(thumbRengi(blok), c)).toBeGreaterThanOrEqual(THRESHOLD)
|
|
266
266
|
})
|
|
267
267
|
}
|
|
268
268
|
}
|
|
@@ -270,26 +270,26 @@ describe('#454 — KABUL KRİTERİ: her varyantın checked track’i iki eksende
|
|
|
270
270
|
|
|
271
271
|
describe('#454 — klon filtresi: varyantlar birbirinden ayırt edilebilir', () => {
|
|
272
272
|
for (const blok of ['light', 'dark'] as const) {
|
|
273
|
-
it(`${blok} — tüm varyant çiftleri ΔE*ab ≥ ${
|
|
274
|
-
for (let i = 0; i <
|
|
275
|
-
for (let j = i + 1; j <
|
|
276
|
-
const
|
|
273
|
+
it(`${blok} — tüm varyant çiftleri ΔE*ab ≥ ${DELTAE_THRESHOLD}`, () => {
|
|
274
|
+
for (let i = 0; i < VARIANTS.length; i++) {
|
|
275
|
+
for (let j = i + 1; j < VARIANTS.length; j++) {
|
|
276
|
+
const pair = `${VARIANTS[i]}↔${VARIANTS[j]}`
|
|
277
277
|
const d = deltaE(
|
|
278
|
-
checkedTrackRengi(
|
|
279
|
-
checkedTrackRengi(
|
|
278
|
+
checkedTrackRengi(VARIANTS[i], blok),
|
|
279
|
+
checkedTrackRengi(VARIANTS[j], blok)
|
|
280
280
|
)
|
|
281
281
|
// Çift adı hata mesajına girsin diye dizge karşılaştırması
|
|
282
|
-
expect(`${
|
|
282
|
+
expect(`${pair}: ${d >= DELTAE_THRESHOLD ? 'ayrık' : d.toFixed(1)}`).toBe(`${pair}: ayrık`)
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
})
|
|
286
286
|
|
|
287
287
|
it(`${blok} — checked track, UNCHECKED track’ten (bg-input) ayırt edilebilir`, () => {
|
|
288
288
|
// Açık switch'in kapalıya benzemesi tam olarak bu issue'nun şikâyeti.
|
|
289
|
-
const unchecked =
|
|
290
|
-
for (const
|
|
291
|
-
expect(deltaE(checkedTrackRengi(
|
|
292
|
-
|
|
289
|
+
const unchecked = semanticRgb('input', blok)
|
|
290
|
+
for (const variantVal of VARIANTS) {
|
|
291
|
+
expect(deltaE(checkedTrackRengi(variantVal, blok), unchecked)).toBeGreaterThanOrEqual(
|
|
292
|
+
DELTAE_THRESHOLD
|
|
293
293
|
)
|
|
294
294
|
}
|
|
295
295
|
})
|
|
@@ -299,8 +299,8 @@ describe('#454 — klon filtresi: varyantlar birbirinden ayırt edilebilir', ()
|
|
|
299
299
|
describe('#454 — `secondary` kararı: ad ↔ token tutarsızlığı ve reddedilen alternatifler', () => {
|
|
300
300
|
it('ESKİ hâli (`bg-accent`) üç zeminde de eşiğin ALTINDA — kusur gerçek', () => {
|
|
301
301
|
for (const blok of ['light', 'dark'] as const) {
|
|
302
|
-
for (const [, z] of
|
|
303
|
-
expect(kontrast(
|
|
302
|
+
for (const [, z] of backgrounds(blok)) {
|
|
303
|
+
expect(kontrast(semanticRgb('accent', blok), z)).toBeLessThan(THRESHOLD)
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
})
|
|
@@ -308,8 +308,8 @@ describe('#454 — `secondary` kararı: ad ↔ token tutarsızlığı ve reddedi
|
|
|
308
308
|
it('SEMANTİK `bg-secondary` ELENDİ — light’ta `--accent` ile BİREBİR aynı, dark’ta daha kötü', () => {
|
|
309
309
|
// "Adı secondary ise bg-secondary kullansın" en bariz alternatif; ölçümle elendi.
|
|
310
310
|
expect(tokenOku('secondary', 'light')).toBe(tokenOku('accent', 'light'))
|
|
311
|
-
expect(kontrast(
|
|
312
|
-
expect(kontrast(
|
|
311
|
+
expect(kontrast(semanticRgb('secondary', 'light'), semanticRgb('background', 'light'))).toBeLessThan(THRESHOLD)
|
|
312
|
+
expect(kontrast(semanticRgb('secondary', 'dark'), semanticRgb('background', 'dark'))).toBeLessThan(1.05)
|
|
313
313
|
})
|
|
314
314
|
|
|
315
315
|
it('`bg-secondary-foreground` ELENDİ — dark’ta `--primary` ile BİREBİR aynı (primary klonu)', () => {
|
|
@@ -317,30 +317,30 @@ describe('#454 — `secondary` kararı: ad ↔ token tutarsızlığı ve reddedi
|
|
|
317
317
|
})
|
|
318
318
|
|
|
319
319
|
it('slate skalasında iki temayı birden geçen VE klon olmayan TEK kademe 500', () => {
|
|
320
|
-
const
|
|
320
|
+
const suitable = KADEMELER.filter((k) => {
|
|
321
321
|
const ham = tokenOku(`secondary-${k}`, 'light')
|
|
322
322
|
if (!ham) return false
|
|
323
323
|
const c = rgbTokenRgb(ham)
|
|
324
324
|
return (['light', 'dark'] as const).every(
|
|
325
325
|
(blok) =>
|
|
326
|
-
|
|
326
|
+
passesAxes(c, blok) &&
|
|
327
327
|
(['primary', 'success', 'warning', 'destructive'] as Varyant[]).every(
|
|
328
|
-
(v) => deltaE(c, checkedTrackRengi(v, blok)) >=
|
|
328
|
+
(v) => deltaE(c, checkedTrackRengi(v, blok)) >= DELTAE_THRESHOLD
|
|
329
329
|
)
|
|
330
330
|
)
|
|
331
331
|
})
|
|
332
|
-
expect(
|
|
332
|
+
expect(suitable).toEqual([500])
|
|
333
333
|
})
|
|
334
334
|
|
|
335
335
|
it('#448’in slider’daki 500-elemesi BURADA GEÇERSİZ — switch’in `accent` varyantı YOK', () => {
|
|
336
336
|
// #453 ölçümü: `--secondary-500` ↔ slider'ın `secondary` varyantı ΔE = 0.0 (klon).
|
|
337
337
|
// Switch'te o çakışma yok; kaynak varyant listesi bunu kanıtlar.
|
|
338
|
-
expect(
|
|
338
|
+
expect(VARIANTS).not.toContain('accent')
|
|
339
339
|
expect(TRACK_CVA).not.toMatch(/\n\s*accent:/)
|
|
340
340
|
const k500 = rgbTokenRgb(tokenOku('secondary-500', 'light')!)
|
|
341
341
|
for (const blok of ['light', 'dark'] as const) {
|
|
342
342
|
for (const v of ['primary', 'success', 'warning', 'destructive'] as Varyant[]) {
|
|
343
|
-
expect(deltaE(k500, checkedTrackRengi(v, blok))).toBeGreaterThanOrEqual(
|
|
343
|
+
expect(deltaE(k500, checkedTrackRengi(v, blok))).toBeGreaterThanOrEqual(DELTAE_THRESHOLD)
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
})
|
|
@@ -361,26 +361,26 @@ describe('#454 — `warning` kardeş kusuru: kök neden ve çözüm şekli', ()
|
|
|
361
361
|
})
|
|
362
362
|
|
|
363
363
|
it('ESKİ hâli (`bg-warning`) LIGHT’ta eşiğin altında, DARK’ta değil', () => {
|
|
364
|
-
expect(kontrast(
|
|
365
|
-
expect(kontrast(
|
|
364
|
+
expect(kontrast(semanticRgb('warning', 'light'), semanticRgb('background', 'light'))).toBeLessThan(THRESHOLD)
|
|
365
|
+
expect(kontrast(semanticRgb('warning', 'dark'), semanticRgb('background', 'dark'))).toBeGreaterThan(THRESHOLD)
|
|
366
366
|
})
|
|
367
367
|
|
|
368
368
|
it('tema-KÖR bir warning kademesi seçilebilirdi ama DARK’ı gereksizce oynatırdı', () => {
|
|
369
369
|
// `dark:` ayrımının gerekçesi: dark zaten geçiyordu, regresyon yüzeyi sıfır tutuldu.
|
|
370
|
-
const
|
|
370
|
+
const themeBlind = KADEMELER.filter((k) => {
|
|
371
371
|
const ham = tokenOku(`warning-${k}`, 'light')
|
|
372
|
-
return ham && (['light', 'dark'] as const).every((b) =>
|
|
372
|
+
return ham && (['light', 'dark'] as const).every((b) => passesAxes(rgbTokenRgb(ham), b))
|
|
373
373
|
})
|
|
374
|
-
expect(
|
|
375
|
-
expect(checkedTrackRengi('warning', 'dark')).toEqual(
|
|
374
|
+
expect(themeBlind).toEqual([700])
|
|
375
|
+
expect(checkedTrackRengi('warning', 'dark')).toEqual(semanticRgb('warning', 'dark'))
|
|
376
376
|
})
|
|
377
377
|
|
|
378
378
|
it('LIGHT’ta eşiği geçen kademe kümesi {700,800,900,950} — en açığı seçildi', () => {
|
|
379
|
-
const
|
|
379
|
+
const passedCount = KADEMELER.filter((k) => {
|
|
380
380
|
const ham = tokenOku(`warning-${k}`, 'light')
|
|
381
|
-
return ham &&
|
|
381
|
+
return ham && passesAxes(rgbTokenRgb(ham), 'light')
|
|
382
382
|
})
|
|
383
|
-
expect(
|
|
383
|
+
expect(passedCount).toEqual([700, 800, 900, 950])
|
|
384
384
|
expect(checkedTrackRengi('warning', 'light')).toEqual(
|
|
385
385
|
rgbTokenRgb(tokenOku('warning-700', 'light')!)
|
|
386
386
|
)
|
|
@@ -389,34 +389,34 @@ describe('#454 — `warning` kardeş kusuru: kök neden ve çözüm şekli', ()
|
|
|
389
389
|
it('DARK render BİT-BİT AYNI kaldı (warning + dokunulmayan varyantlar)', () => {
|
|
390
390
|
for (const v of ['primary', 'success', 'warning', 'destructive'] as Varyant[]) {
|
|
391
391
|
const tok = v === 'destructive' ? 'error' : v
|
|
392
|
-
expect(checkedTrackRengi(v, 'dark')).toEqual(
|
|
392
|
+
expect(checkedTrackRengi(v, 'dark')).toEqual(semanticRgb(tok, 'dark'))
|
|
393
393
|
}
|
|
394
394
|
})
|
|
395
395
|
})
|
|
396
396
|
|
|
397
397
|
describe('#454 — kaynak sözleşmesi (switch.tsx)', () => {
|
|
398
398
|
it('`secondary` → slate skalasının 500 kademesi, `dark:` ayrımı YOK', () => {
|
|
399
|
-
expect(
|
|
399
|
+
expect(variantClass('secondary')).toBe('data-[state=checked]:bg-[rgb(var(--secondary-500))]')
|
|
400
400
|
})
|
|
401
401
|
|
|
402
402
|
it('`warning` → light -700, dark semantik token', () => {
|
|
403
|
-
expect(
|
|
403
|
+
expect(variantClass('warning')).toBe(
|
|
404
404
|
'data-[state=checked]:bg-[rgb(var(--warning-700))] dark:data-[state=checked]:bg-warning'
|
|
405
405
|
)
|
|
406
406
|
})
|
|
407
407
|
|
|
408
408
|
it('KAPSAM KİLİDİ: geçen üç varyant DOKUNULMADI', () => {
|
|
409
|
-
expect(
|
|
410
|
-
expect(
|
|
411
|
-
expect(
|
|
409
|
+
expect(variantClass('primary')).toBe('data-[state=checked]:bg-primary')
|
|
410
|
+
expect(variantClass('success')).toBe('data-[state=checked]:bg-success')
|
|
411
|
+
expect(variantClass('destructive')).toBe('data-[state=checked]:bg-error')
|
|
412
412
|
})
|
|
413
413
|
|
|
414
414
|
it('KAPSAM KİLİDİ: unchecked track ve prop yüzeyi DEĞİŞMEDİ', () => {
|
|
415
415
|
expect(TRACK_CVA).toContain('data-[state=unchecked]:bg-input')
|
|
416
|
-
expect(
|
|
416
|
+
expect(source).toContain(
|
|
417
417
|
'type SwitchVariant = "primary" | "success" | "warning" | "destructive" | "secondary";'
|
|
418
418
|
)
|
|
419
|
-
expect(
|
|
419
|
+
expect(source).toContain('export { Switch, switchVariants }')
|
|
420
420
|
})
|
|
421
421
|
|
|
422
422
|
it('`bg-secondary-500` YAZILMADI — preset secondary skalasını hâlâ açmıyor (#307 ölü-sınıf tuzağı)', () => {
|
|
@@ -453,31 +453,31 @@ describe('#454 — kaynak sözleşmesi (switch.tsx)', () => {
|
|
|
453
453
|
})
|
|
454
454
|
|
|
455
455
|
describe('#454 — render sözleşmesi (<Switch />)', () => {
|
|
456
|
-
const
|
|
456
|
+
const cls = (jsx: React.ReactElement) => {
|
|
457
457
|
const { container } = render(jsx)
|
|
458
458
|
return (container.querySelector('[role="switch"]') as HTMLElement).className
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
it('checked `secondary` switch slate-500 sınıfını taşıyor', () => {
|
|
462
|
-
expect(
|
|
462
|
+
expect(cls(<Switch variant="secondary" defaultChecked />)).toContain(
|
|
463
463
|
'data-[state=checked]:bg-[rgb(var(--secondary-500))]'
|
|
464
464
|
)
|
|
465
465
|
})
|
|
466
466
|
|
|
467
467
|
it('checked `secondary` switch ARTIK `bg-accent` taşımıyor', () => {
|
|
468
|
-
expect(
|
|
468
|
+
expect(cls(<Switch variant="secondary" defaultChecked />)).not.toContain(
|
|
469
469
|
'data-[state=checked]:bg-accent'
|
|
470
470
|
)
|
|
471
471
|
})
|
|
472
472
|
|
|
473
473
|
it('checked `warning` switch light -700 + dark semantik sınıflarını birlikte taşıyor', () => {
|
|
474
|
-
const c =
|
|
474
|
+
const c = cls(<Switch variant="warning" defaultChecked />)
|
|
475
475
|
expect(c).toContain('data-[state=checked]:bg-[rgb(var(--warning-700))]')
|
|
476
476
|
expect(c).toContain('dark:data-[state=checked]:bg-warning')
|
|
477
477
|
})
|
|
478
478
|
|
|
479
479
|
it('varsayılan varyant (primary) ve unchecked davranışı korunuyor', () => {
|
|
480
|
-
const c =
|
|
480
|
+
const c = cls(<Switch />)
|
|
481
481
|
expect(c).toContain('data-[state=checked]:bg-primary')
|
|
482
482
|
expect(c).toContain('data-[state=unchecked]:bg-input')
|
|
483
483
|
})
|
|
@@ -482,6 +482,128 @@ describe('Tooltip Components', () => {
|
|
|
482
482
|
})
|
|
483
483
|
})
|
|
484
484
|
|
|
485
|
+
// #650 — WCAG 1.4.13 "Content on Hover or Focus"
|
|
486
|
+
//
|
|
487
|
+
// Radix `TooltipContentImpl` KOŞULSUZ bir "kaydırma → kapat" etkisi kurar:
|
|
488
|
+
// window.addEventListener("scroll", e => {
|
|
489
|
+
// if (e.target?.contains(trigger)) onClose()
|
|
490
|
+
// }, { capture: true })
|
|
491
|
+
//
|
|
492
|
+
// Görünür alanın dışındaki bir tetikleyiciye `Tab` ile gelindiğinde tarayıcı
|
|
493
|
+
// elemanı görünür alana kaydırır → tooltip açıldığı anda kapanır. Gerçek
|
|
494
|
+
// tarayıcıda ölçülen sıra (docs sayfası, 365 gerçek `Tab` basışı):
|
|
495
|
+
// FOCUSIN → data-state=instant-open (+4.5 ms) → SCROLL → closed (+6.4 ms)
|
|
496
|
+
//
|
|
497
|
+
// Aşağıdaki testler o zincirin tamamını jsdom'da yeniden üretir: gerçek odak
|
|
498
|
+
// (userEvent.tab → jsdom `focus`+`focusin`) + tetikleyiciyi İÇEREN bir hedefte
|
|
499
|
+
// kaydırma olayı. Düzeltme geri alınırsa ilk test kırmızıya döner.
|
|
500
|
+
describe('#650 Klavye Odağı Kalıcılığı (WCAG 1.4.13)', () => {
|
|
501
|
+
// Tetikleyiciyi kapsayan bir hedefte kaydırma üret. Radix dinleyicisi
|
|
502
|
+
// `window` üzerinde YAKALAMA fazındadır; `document`e gönderilen olay
|
|
503
|
+
// oraya `target === document` ile ulaşır ve `document.contains(trigger)`
|
|
504
|
+
// doğru olduğu için kapatma tetiklenir.
|
|
505
|
+
const scrollTriggerIntoView = () => {
|
|
506
|
+
fireEvent.scroll(document)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
it('klavye odağı tooltip"i açar', async () => {
|
|
510
|
+
const user = userEvent.setup()
|
|
511
|
+
render(
|
|
512
|
+
<Tooltip content="Keyboard reachable" delayDuration={0}>
|
|
513
|
+
<button>Focus me</button>
|
|
514
|
+
</Tooltip>
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
await user.tab()
|
|
518
|
+
expect(screen.getByRole('button')).toHaveFocus()
|
|
519
|
+
|
|
520
|
+
expect(await screen.findByRole('tooltip')).toHaveTextContent('Keyboard reachable')
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('odak kaydırması tooltip"i KAPATMAZ (asıl #650 gerilemesi)', async () => {
|
|
524
|
+
const user = userEvent.setup()
|
|
525
|
+
render(
|
|
526
|
+
<Tooltip content="Keyboard reachable" delayDuration={0}>
|
|
527
|
+
<button>Focus me</button>
|
|
528
|
+
</Tooltip>
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
await user.tab()
|
|
532
|
+
expect(await screen.findByRole('tooltip')).toBeInTheDocument()
|
|
533
|
+
|
|
534
|
+
// Tarayıcının odaklanan elemanı görünür alana taşıması
|
|
535
|
+
scrollTriggerIntoView()
|
|
536
|
+
scrollTriggerIntoView() // yumuşak kaydırma birden çok olay üretir
|
|
537
|
+
|
|
538
|
+
// Odak hâlâ tetikleyicide → içerik görünür KALMALI ("Persistent")
|
|
539
|
+
await waitFor(() => {
|
|
540
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument()
|
|
541
|
+
})
|
|
542
|
+
expect(screen.getByRole('button')).toHaveFocus()
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('Escape klavye odağındaki tooltip"i kapatır ("Dismissible")', async () => {
|
|
546
|
+
const user = userEvent.setup()
|
|
547
|
+
render(
|
|
548
|
+
<Tooltip content="Keyboard reachable" delayDuration={0}>
|
|
549
|
+
<button>Focus me</button>
|
|
550
|
+
</Tooltip>
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
await user.tab()
|
|
554
|
+
expect(await screen.findByRole('tooltip')).toBeInTheDocument()
|
|
555
|
+
|
|
556
|
+
await user.keyboard('{Escape}')
|
|
557
|
+
|
|
558
|
+
await waitFor(() => {
|
|
559
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
|
560
|
+
})
|
|
561
|
+
// Odak tetikleyicide kalır — Escape yalnız içeriği kapatır
|
|
562
|
+
expect(screen.getByRole('button')).toHaveFocus()
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('odak ayrıldığında tooltip yine kapanır (kilit sızmıyor)', async () => {
|
|
566
|
+
const user = userEvent.setup()
|
|
567
|
+
render(
|
|
568
|
+
<div>
|
|
569
|
+
<Tooltip content="Keyboard reachable" delayDuration={0}>
|
|
570
|
+
<button>Focus me</button>
|
|
571
|
+
</Tooltip>
|
|
572
|
+
<button>Next</button>
|
|
573
|
+
</div>
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
await user.tab()
|
|
577
|
+
expect(await screen.findByRole('tooltip')).toBeInTheDocument()
|
|
578
|
+
|
|
579
|
+
await user.tab() // sonraki tetiklenebilir elemana geç
|
|
580
|
+
expect(screen.getByRole('button', { name: 'Next' })).toHaveFocus()
|
|
581
|
+
|
|
582
|
+
await waitFor(() => {
|
|
583
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
|
584
|
+
})
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('FARE ile açılan tooltip kaydırmada kapanmaya devam eder (kilit klavyeye özgü)', async () => {
|
|
588
|
+
const user = userEvent.setup()
|
|
589
|
+
render(
|
|
590
|
+
<Tooltip content="Pointer opened" delayDuration={0}>
|
|
591
|
+
<button>Hover me</button>
|
|
592
|
+
</Tooltip>
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
await openByHover(user, screen.getByRole('button'))
|
|
596
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument()
|
|
597
|
+
|
|
598
|
+
// Odak hiç oluşmadı → kilit yok → Radix'in kaydırma davranışı korunur
|
|
599
|
+
scrollTriggerIntoView()
|
|
600
|
+
|
|
601
|
+
await waitFor(() => {
|
|
602
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
|
|
485
607
|
describe('Controlled State', () => {
|
|
486
608
|
it('renders open when defaultOpen is set and reports changes', async () => {
|
|
487
609
|
const onOpenChange = jest.fn()
|
|
@@ -20,7 +20,12 @@ const AccordionItem = React.forwardRef<
|
|
|
20
20
|
>(({ className, ...props }, ref) => (
|
|
21
21
|
<AccordionPrimitive.Item
|
|
22
22
|
ref={ref}
|
|
23
|
-
|
|
23
|
+
// #661-17: ham `border-gray-200 dark:border-gray-800` → `border-border`.
|
|
24
|
+
// Pro ikizi (accordion.tsx:21-23) zaten `border-border` kullanıyor; free
|
|
25
|
+
// taraf ham palette kalınca tema/marka özelleştirmesi bu kenarlığa
|
|
26
|
+
// İŞLEMİYORDU. Token değerleri görsel olarak denk: --border açık temada
|
|
27
|
+
// 214.3 31.8% 91.4% (≈ gray-200), koyu temada 216 34% 17% (≈ gray-800).
|
|
28
|
+
className={cn("moonui-theme", "border-b border-border", className)}
|
|
24
29
|
{...props}
|
|
25
30
|
/>
|
|
26
31
|
))
|
|
@@ -34,7 +39,9 @@ const AccordionTrigger = React.forwardRef<
|
|
|
34
39
|
<AccordionPrimitive.Trigger
|
|
35
40
|
ref={ref}
|
|
36
41
|
className={cn(
|
|
37
|
-
|
|
42
|
+
// #661-17: `dark:text-gray-100` KALDIRILDI — tetikleyici artık iki temada
|
|
43
|
+
// da `--foreground`u miras alır (koyu temada 213 31% 91% ≈ gray-100).
|
|
44
|
+
"flex flex-1 items-center justify-between py-4 font-medium transition-all",
|
|
38
45
|
"hover:underline hover:text-primary",
|
|
39
46
|
"[&[data-state=open]>svg]:rotate-180",
|
|
40
47
|
className
|
|
@@ -55,7 +62,13 @@ const AccordionContent = React.forwardRef<
|
|
|
55
62
|
<AccordionPrimitive.Content
|
|
56
63
|
ref={ref}
|
|
57
64
|
className={cn(
|
|
58
|
-
|
|
65
|
+
// #661-17: ham `text-gray-700 dark:text-gray-300` → `text-foreground/80`.
|
|
66
|
+
// Ölçüldü (iki tema, kart zemini): açık 10.30 → 11.48, koyu 12.19 → 9.44.
|
|
67
|
+
// İkisi de AA'nın (4.5) çok üstünde, yani eşik geçilmiyor — `#545 ihlal
|
|
68
|
+
// takası` sınıfı bir gerileme YOK. `text-muted-foreground` bilerek
|
|
69
|
+
// SEÇİLMEDİ: açık temada 4.76'ya inip gövde metnini görünür biçimde
|
|
70
|
+
// soldururdu (§9 estetik değişiklik olurdu).
|
|
71
|
+
"overflow-hidden text-sm text-foreground/80 transition-all",
|
|
59
72
|
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
|
60
73
|
className
|
|
61
74
|
)}
|
|
@@ -21,7 +21,7 @@ const alertVariants = cva(
|
|
|
21
21
|
default: "bg-background text-foreground border-border",
|
|
22
22
|
primary: "bg-primary/10 text-primary border-primary/30",
|
|
23
23
|
success: "bg-success/10 text-success border-success/30",
|
|
24
|
-
warning: "bg-warning/10 text-warning border-warning/30",
|
|
24
|
+
warning: "bg-warning/10 text-warning-subtle-foreground border-warning/30",
|
|
25
25
|
error: "bg-destructive/10 text-destructive border-destructive/30",
|
|
26
26
|
// `--info` token'ı (tokens.css) blue-500 ile birebir aynı değeri
|
|
27
27
|
// taşır → görsel değişiklik yok, ancak varyant artık temaya ve
|
|
@@ -25,11 +25,22 @@ const badgeVariants = cva(
|
|
|
25
25
|
// aynı varyant pro pakette token'lıydı → free ve pro `primary` için
|
|
26
26
|
// farklı renk üretiyordu. Artık token'a bağlı (tema/karanlık moda duyarlı).
|
|
27
27
|
// `primary` VARSAYILAN varyant olduğu için görünür bir renk değişimidir.
|
|
28
|
+
// #646: `dark:bg-primary/90` KALDIRILDI. `primary` VARSAYILAN varyant olduğu
|
|
29
|
+
// için varyant verilmeyen HER `<Badge>` bu sınıfı alıyordu; `tailwind-merge`
|
|
30
|
+
// `dark:` önekli arka planı tüketicinin ÖNEKSİZ `bg-*` sınıfıyla aynı grupta
|
|
31
|
+
// görmediğinden ikisi birden hayatta kalıyor ve koyu temada `dark:` özgüllükle
|
|
32
|
+
// kazanıyordu. Sonuç: `<Badge className="bg-green-500/20 text-green-300">`
|
|
33
|
+
// koyu temada near-white zemin + açık yeşil metin = 1.10:1 (ölçüldü,
|
|
34
|
+
// magnetic-button "Pending"; swipe-actions'ta 6 rozet 1.10–1.48 aralığında).
|
|
35
|
+
// ÖNEKSİZ `bg-primary` bırakıldı: tüketici sınıfıyla AYNI grupta çakışır ve
|
|
36
|
+
// doğru şekilde ezilir. Rozetin kendi görünümü BOZULMAZ — koyu temada
|
|
37
|
+
// `bg-primary` + `text-primary-foreground` 16.16 → 19.80'e çıkar
|
|
38
|
+
// (yarı saydamlık kalktığı için); açık temada değişiklik yok.
|
|
28
39
|
primary: [
|
|
29
40
|
"border-transparent bg-primary text-primary-foreground",
|
|
30
41
|
"hover:bg-primary/90",
|
|
31
42
|
"focus-visible:ring-primary/30 dark:focus-visible:ring-primary/40",
|
|
32
|
-
"dark:
|
|
43
|
+
"dark:shadow-inner dark:shadow-primary/10",
|
|
33
44
|
],
|
|
34
45
|
secondary: [
|
|
35
46
|
"border-transparent bg-secondary text-secondary-foreground",
|