@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui",
3
- "version": "6.19.0",
3
+ "version": "6.19.2",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -0,0 +1,331 @@
1
+ /**
2
+ * #424 — Progress `warning` / `error` varyantlarında dolgu↔track kontrast değişmezi.
3
+ *
4
+ * BULGU (#372'nin birebir kardeşi): dolgu `bg-warning` / `bg-error`, track ise
5
+ * `bg-warning/20` / `bg-error/20` — dolgu ile track AYNI token'ın iki alfa değeri.
6
+ * LIGHT temada ayrım kayboluyordu: warning 1.83 · error 2.89 (eşik 3:1, WCAG
7
+ * 1.4.11 metin-dışı). DARK tema zaten temizdi (warning 6.97 · error 4.46), yani
8
+ * bu bulgu YALNIZ light temaya ait — düzeltmenin de dark render'ı değiştirmemesi
9
+ * gerekir. axe bunu yakalamaz: `color-contrast` yalnız METİN öğelerini ölçer.
10
+ *
11
+ * KÖK NEDEN — token seviyesinde eksik tema-uyarlaması:
12
+ * `--warning` ve `--error` tokens.css'te `.dark` bloğunda EZİLMİYOR; iki temada
13
+ * da aynı renk. Kardeşleri tema-uyarlanabilir olduğu için eşiği geçiyor
14
+ * (`--success` light %29 → dark %36). Bu iki aile o uyarlamadan yoksun.
15
+ *
16
+ * ÇÖZÜM: eksik uyarlamayı component düzeyinde ver — light'ta skalanın -700
17
+ * kademesine in, dark'ta semantik token'a dokunma:
18
+ * warning: "bg-[rgb(var(--warning-700))] dark:bg-warning"
19
+ * error: "bg-[rgb(var(--error-700))] dark:bg-error"
20
+ *
21
+ * Tema-KÖR bir -700 ataması (issue gövdesinin uyardığı refleks) dark'ı KIRARDI —
22
+ * aşağıda testle kanıtlanıyor. `#372`'nin tek-orta-ton çözümü (`--secondary-500`)
23
+ * burada UYGULANAMAZ: orada `--secondary` temaya göre değişiyordu, burada
24
+ * değişmiyor; track `/20` iken skalada iki temayı birden geçen kademe YOK
25
+ * (o da aşağıda kanıtlanıyor). Track alfaları bu yüzden `/20` KALDI.
26
+ *
27
+ * Hesap yöntemi progress-secondary-contrast.test.ts ile birebir aynıdır (WCAG 2.x
28
+ * bağıl parlaklık); oradaki "hesaplayıcı doğru mu?" çapası burada da var.
29
+ * NOT: #372'nin dosyasıyla BİLEREK ayrı dosya — o dosya `--secondary` eksenini
30
+ * kilitler, bu dosya `--warning`/`--error` eksenini; çakışma yok.
31
+ */
32
+ import * as React from 'react'
33
+ import { render, screen } from '@testing-library/react'
34
+ import '@testing-library/jest-dom'
35
+ import * as fs from 'fs'
36
+ import * as path from 'path'
37
+ import { Progress } from '../progress'
38
+
39
+ const KOK = path.resolve(__dirname, '../../..')
40
+ const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
41
+
42
+ /** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
43
+ function tokenOku(ad: string, blok: 'light' | 'dark'): string | null {
44
+ const kaynak =
45
+ blok === 'dark'
46
+ ? tokens.slice(tokens.indexOf('.dark'))
47
+ : tokens.slice(0, tokens.indexOf('.dark'))
48
+ return kaynak.match(new RegExp(`--${ad}:\\s*([^;]+);`))?.[1].trim() ?? null
49
+ }
50
+
51
+ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
52
+ h /= 360
53
+ s /= 100
54
+ l /= 100
55
+ const f = (n: number) => {
56
+ const k = (n + h * 12) % 12
57
+ const a = s * Math.min(l, 1 - l)
58
+ return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
59
+ }
60
+ return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
61
+ }
62
+
63
+ const hslTokenRgb = (v: string): [number, number, number] => {
64
+ const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
65
+ return hslToRgb(h, s, l)
66
+ }
67
+
68
+ /**
69
+ * Skala token'ları ("--warning-700: 161 98 7") HSL DEĞİL, RGB üçlüsüdür
70
+ * (semantik token'ların HSL formatından farklı) — component'te bu yüzden
71
+ * `bg-[rgb(var(--warning-700))]` kompozisyonu kullanılır, hsl() değil.
72
+ */
73
+ const rgbTokenRgb = (v: string): [number, number, number] => {
74
+ const p = v.split(/\s+/).map((x) => parseFloat(x))
75
+ return [p[0], p[1], p[2]]
76
+ }
77
+
78
+ function bagilParlaklik([r, g, b]: [number, number, number]): number {
79
+ const c = (v: number) => {
80
+ const x = v / 255
81
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
82
+ }
83
+ return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
84
+ }
85
+
86
+ function rgbKontrast(a: [number, number, number], b: [number, number, number]): number {
87
+ const la = bagilParlaklik(a)
88
+ const lb = bagilParlaklik(b)
89
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la]
90
+ return (hi + 0.05) / (lo + 0.05)
91
+ }
92
+
93
+ /** `bg-warning/20` gibi alfa katmanının zemin üstündeki bileşke rengi */
94
+ function alfaBileske(
95
+ ust: [number, number, number],
96
+ alfa: number,
97
+ zemin: [number, number, number]
98
+ ): [number, number, number] {
99
+ return [
100
+ Math.round(alfa * ust[0] + (1 - alfa) * zemin[0]),
101
+ Math.round(alfa * ust[1] + (1 - alfa) * zemin[1]),
102
+ Math.round(alfa * ust[2] + (1 - alfa) * zemin[2]),
103
+ ]
104
+ }
105
+
106
+ /**
107
+ * Docs kabuğunda ölçülen GERÇEK koyu kart zemini (#111827 = rgb(17,24,39)) —
108
+ * #284/#368 konvansiyonu: kontrast, --background'ın yanında bu gerçek yüzeye
109
+ * göre de ölçülür. Bu zemin bu bulguda DARBOĞAZ: en düşük oranları o veriyor.
110
+ */
111
+ const KART_GERCEK_DARK: [number, number, number] = [17, 24, 39]
112
+
113
+ const AILELER = ['warning', 'error'] as const
114
+ type Aile = (typeof AILELER)[number]
115
+
116
+ /** Semantik token'ın ilgili temadaki değeri — `.dark` ezmesi YOKSA light'a düşer */
117
+ function semantikRgb(aile: Aile, blok: 'light' | 'dark'): [number, number, number] {
118
+ return hslTokenRgb(tokenOku(aile, blok) ?? tokenOku(aile, 'light')!)
119
+ }
120
+
121
+ /** O temada track'in oluşabileceği tüm zeminler */
122
+ function zeminler(blok: 'light' | 'dark'): Array<[string, [number, number, number]]> {
123
+ const z: Array<[string, [number, number, number]]> = [
124
+ ['--background', hslTokenRgb(tokenOku('background', blok)!)],
125
+ ['--card', hslTokenRgb(tokenOku('card', blok)!)],
126
+ ]
127
+ if (blok === 'dark') z.push(['gerçek kart #111827', KART_GERCEK_DARK])
128
+ return z
129
+ }
130
+
131
+ // ——— Renkler KAYNAKTAN türetilir ———————————————————————————————————————
132
+ // Ölçümü `bg-...-700` varsayımına göre değil, progress.tsx'in O ANKİ sınıf
133
+ // dizgelerine göre yapıyoruz. Böylece aşağıdaki ≥3:1 testleri component'e
134
+ // GERÇEKTEN bağlanır: biri dolguyu eski hâline döndürürse (ya da track alfasını
135
+ // oynatırsa) kriter testi kırmızı olur — yalnız sözleşme testleri değil.
136
+ const kaynak = fs.readFileSync(path.join(KOK, 'components/ui/progress.tsx'), 'utf8')
137
+ const AYRAC = kaynak.indexOf('progressIndicatorVariants')
138
+ const TRACK_BLOK = kaynak.slice(0, AYRAC)
139
+ const DOLGU_BLOK = kaynak.slice(AYRAC)
140
+
141
+ /** CVA bloğundan `<aile>: "<sınıflar>"` değerini oku */
142
+ function varyantSinifi(blok: string, aile: Aile): string {
143
+ const m = blok.match(new RegExp(`\\n\\s*${aile}:\\s*"([^"]+)"`))
144
+ if (!m) throw new Error(`${aile} varyantı bulunamadı`)
145
+ return m[1]
146
+ }
147
+
148
+ /** Tek bir Tailwind bg sınıfını renge çevir (arbitrary skala VEYA semantik token) */
149
+ function sinifRengi(sinif: string, blok: 'light' | 'dark'): [number, number, number] {
150
+ const skala = sinif.match(/^bg-\[rgb\(var\(--([a-z0-9-]+)\)\)\]$/)
151
+ if (skala) return rgbTokenRgb(tokenOku(skala[1], 'light')!) // skala yalnız :root'ta
152
+ const semantik = sinif.match(/^bg-([a-z-]+)$/)
153
+ if (semantik) return hslTokenRgb(tokenOku(semantik[1], blok) ?? tokenOku(semantik[1], 'light')!)
154
+ throw new Error(`çözülemeyen sınıf: ${sinif}`)
155
+ }
156
+
157
+ /** Dolgunun o temada GERÇEKTEN uygulanan rengi — `dark:` öneki dark'ta kazanır */
158
+ function dolguRengi(aile: Aile, blok: 'light' | 'dark'): [number, number, number] {
159
+ const siniflar = varyantSinifi(DOLGU_BLOK, aile).split(/\s+/)
160
+ const darkSinif = siniflar.find((s) => s.startsWith('dark:'))?.slice('dark:'.length)
161
+ const lightSinif = siniflar.find((s) => !s.startsWith('dark:'))
162
+ if (!lightSinif) throw new Error(`${aile}: temel (light) sınıf yok`)
163
+ return sinifRengi(blok === 'dark' ? darkSinif ?? lightSinif : lightSinif, blok)
164
+ }
165
+
166
+ /** Track'in o zemin üstündeki bileşke rengi — alfa da kaynaktan okunur */
167
+ function trackler(aile: Aile, blok: 'light' | 'dark') {
168
+ const sinif = varyantSinifi(TRACK_BLOK, aile)
169
+ const m = sinif.match(/^bg-([a-z-]+)\/(\d+)$/)
170
+ if (!m) throw new Error(`track sınıfı çözülemedi: ${sinif}`)
171
+ const taban = hslTokenRgb(tokenOku(m[1], blok) ?? tokenOku(m[1], 'light')!)
172
+ const alfa = Number(m[2]) / 100
173
+ return zeminler(blok).map(([ad, z]) => [ad, alfaBileske(taban, alfa, z)] as const)
174
+ }
175
+
176
+ const KADEMELER = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
177
+
178
+ describe('#424 progress warning/error dolgu↔track kontrastı', () => {
179
+ it('hesaplayıcı DOĞRU — bilinen bir çift belgelenen oranı veriyor', () => {
180
+ // Sessiz-yeşil koruması: `--info-subtle` çifti tokens.css'te 9.52:1 diye
181
+ // belgelenmiş; hesaplayıcı bozulursa bu çapa kırmızı olur.
182
+ const oran = rgbKontrast(
183
+ hslTokenRgb(tokenOku('info-subtle', 'light')!),
184
+ hslTokenRgb(tokenOku('info-subtle-foreground', 'light')!)
185
+ )
186
+ expect(oran).toBeGreaterThan(9.3)
187
+ expect(oran).toBeLessThan(9.7)
188
+ })
189
+
190
+ it('hesaplayıcı, denetimin TARAYICIDA ölçtüğü renkleri birebir üretiyor', () => {
191
+ // Çapraz doğrulama: issue #424 gövdesindeki gerçek tarayıcı ölçümleri
192
+ // warning dolgu rgb(245,159,10) / track rgb(253,236,206), error dolgu
193
+ // rgb(239,67,67) / track rgb(252,217,217). Model bunları tutturamıyorsa
194
+ // aşağıdaki tüm oranlar anlamsızdır.
195
+ expect(semantikRgb('warning', 'light')).toEqual([245, 159, 10])
196
+ expect(semantikRgb('error', 'light')).toEqual([239, 67, 67])
197
+ expect(trackler('warning', 'light')[0][1]).toEqual([253, 236, 206])
198
+ expect(trackler('error', 'light')[0][1]).toEqual([252, 217, 217])
199
+ })
200
+
201
+ describe('kök neden: --warning ve --error `.dark` bloğunda EZİLMİYOR', () => {
202
+ it.each(AILELER)('--%s dark ezmesi yok → iki temada aynı renk', (aile) => {
203
+ // Bu, bulgunun yapısal kök nedeni. Biri ileride `.dark` bloğuna
204
+ // `--warning`/`--error` eklerse bu test kırmızı olur ve component
205
+ // düzeyindeki `dark:` telafisinin yeniden ölçülmesi gerektiğini söyler.
206
+ expect(tokenOku(aile, 'dark')).toBeNull()
207
+ expect(semantikRgb(aile, 'light')).toEqual(semantikRgb(aile, 'dark'))
208
+ })
209
+
210
+ it('karşıtlık: --success tema-uyarlanabilir olduğu İÇİN eşiği geçiyor', () => {
211
+ // Neden success'e dokunulmadığının kanıtı: onun dark ezmesi VAR.
212
+ expect(tokenOku('success', 'dark')).not.toBeNull()
213
+ expect(tokenOku('success', 'light')).not.toBe(tokenOku('success', 'dark'))
214
+ })
215
+
216
+ it.each(AILELER)('--%s-700 tanımlı ve RGB üçlüsü olarak ayrıştırılabiliyor', (aile) => {
217
+ const ham = tokenOku(`${aile}-700`, 'light')
218
+ expect(ham).toBeTruthy()
219
+ rgbTokenRgb(ham!).forEach((kanal) => {
220
+ expect(Number.isFinite(kanal)).toBe(true)
221
+ expect(kanal).toBeGreaterThanOrEqual(0)
222
+ expect(kanal).toBeLessThanOrEqual(255)
223
+ })
224
+ })
225
+ })
226
+
227
+ describe('kaynak sözleşmesi: dolgu ile track FARKLI renklere basıyor', () => {
228
+ it.each(AILELER)('track %s sınıfı `/20` alfasını KORUYOR (altı varyantın ortak deseni)', (aile) => {
229
+ expect(kaynak).toMatch(new RegExp(`${aile}:\\s*"bg-${aile}\\/20"`))
230
+ })
231
+
232
+ it.each(AILELER)('dolgu %s sınıfı light\'ta -700, dark\'ta semantik token', (aile) => {
233
+ expect(kaynak).toMatch(
234
+ new RegExp(`${aile}:\\s*"bg-\\[rgb\\(var\\(--${aile}-700\\)\\)\\] dark:bg-${aile}"`)
235
+ )
236
+ })
237
+ })
238
+
239
+ describe('render sözleşmesi', () => {
240
+ it.each(AILELER)('<Progress variant="%s" /> — track ve dolgu DOM\'a ayrışmış iniyor', (aile) => {
241
+ render(React.createElement(Progress, { variant: aile, value: 50 }))
242
+ const track = screen.getByRole('progressbar')
243
+ expect(track).toHaveClass(`bg-${aile}/20`)
244
+ const dolgu = track.firstElementChild as HTMLElement
245
+ expect(dolgu).not.toBeNull()
246
+ expect(dolgu.className).toContain(`bg-[rgb(var(--${aile}-700))]`)
247
+ expect(dolgu.className).toContain(`dark:bg-${aile}`)
248
+ // Light'ta semantik token dolguya UYGULANMAMALI (track'in aynısıydı):
249
+ // yalnız `dark:` önekli hâli kalmalı.
250
+ expect(dolgu.className).not.toMatch(new RegExp(`(?:^|\\s)bg-${aile}(?:\\s|$)`))
251
+ })
252
+
253
+ it.each(AILELER)('#318 düşüş mantığı KORUNUYOR — indicatorVariant hâlâ %s varyantını ezer', (aile) => {
254
+ render(
255
+ React.createElement(Progress, { variant: aile, indicatorVariant: 'success', value: 50 })
256
+ )
257
+ const dolgu = screen.getByRole('progressbar').firstElementChild as HTMLElement
258
+ expect(dolgu.className).toContain('bg-success')
259
+ expect(dolgu.className).not.toContain(`--${aile}-700`)
260
+ })
261
+ })
262
+
263
+ describe.each(AILELER)('%s — kabul kriteri ≥3:1 (WCAG 1.4.11)', (aile) => {
264
+ it.each(['light', 'dark'] as const)('%s temada TÜM zeminlerde eşiği geçiyor', (blok) => {
265
+ // Ölçülen: warning light 4.24 · dark 6.97 (docs kartı 5.77)
266
+ // error light 4.95 · dark 4.46 (docs kartı 3.82)
267
+ const dolgu = dolguRengi(aile, blok)
268
+ for (const [ad, track] of trackler(aile, blok)) {
269
+ const oran = rgbKontrast(dolgu, track)
270
+ expect({ zemin: ad, gecti: oran >= 3.0 }).toEqual({ zemin: ad, gecti: true })
271
+ }
272
+ })
273
+
274
+ it('ESKİ dolgu (semantik token) LIGHT temada başarısızdı — bug\'ın belgesi', () => {
275
+ // warning 1.83 · error 2.89. Bu test düzeltmenin GEREKLİLİĞİNİ sabitler:
276
+ // biri dolguyu `bg-warning`/`bg-error`e geri döndürürse üstteki kriter
277
+ // testi kırmızı olur, burası da nedenini belgeler.
278
+ const eski = semantikRgb(aile, 'light')
279
+ for (const [, track] of trackler(aile, 'light')) {
280
+ expect(rgbKontrast(eski, track)).toBeLessThan(3.0)
281
+ }
282
+ })
283
+
284
+ it('DARK render değişmedi — eski dolgu dark\'ta zaten geçiyordu ve hâlâ o renk', () => {
285
+ // Bulgu light'a özgüydü; düzeltmenin dark'ta hiçbir şeyi oynatmaması
286
+ // bilinçli bir kısıt (regresyon yüzeyini sıfıra indirir).
287
+ expect(dolguRengi(aile, 'dark')).toEqual(semantikRgb(aile, 'dark'))
288
+ for (const [, track] of trackler(aile, 'dark')) {
289
+ expect(rgbKontrast(semantikRgb(aile, 'dark'), track)).toBeGreaterThanOrEqual(3.0)
290
+ }
291
+ })
292
+ })
293
+
294
+ describe('seçim gerekçesi — reddedilen alternatifler', () => {
295
+ it.each(AILELER)('TEMA-KÖR `-700` ataması %s varyantını DARK\'ta kırardı', (aile) => {
296
+ // Issue gövdesinin uyardığı refleks. -700 kademesi dark track'e karşı
297
+ // (gerçek docs kartı zemininde) eşiğin ALTINDA: warning 2.50 · error 2.23.
298
+ // `dark:` ayrımı tam olarak bunu önlüyor.
299
+ const k700 = rgbTokenRgb(tokenOku(`${aile}-700`, 'light')!)
300
+ const docsTrack = trackler(aile, 'dark').find(([ad]) => ad === 'gerçek kart #111827')![1]
301
+ expect(rgbKontrast(k700, docsTrack)).toBeLessThan(3.0)
302
+ })
303
+
304
+ it.each(AILELER)('#372\'nin tek-orta-ton çözümü %s için UYGULANAMAZ — geçen kademe YOK', (aile) => {
305
+ // #372 dolguyu tek bir skala kademesine (`--secondary-500`) sabitleyip iki
306
+ // temayı birden geçebilmişti, çünkü `--secondary` temaya göre DEĞİŞİYOR.
307
+ // Burada semantik token sabit olduğundan track'ler birbirinden çok uzak:
308
+ // skalanın 11 kademesinin HİÇBİRİ tema-kör olarak iki temayı birden
309
+ // geçemiyor. Bu, `dark:` ayrımını zorunlu kılan kanıttır.
310
+ const tumTrackler = [...trackler(aile, 'light'), ...trackler(aile, 'dark')]
311
+ const gecen = KADEMELER.filter((k) => {
312
+ const ham = tokenOku(`${aile}-${k}`, 'light')
313
+ if (!ham) return false
314
+ const d = rgbTokenRgb(ham)
315
+ return tumTrackler.every(([, t]) => rgbKontrast(d, t) >= 3.0)
316
+ })
317
+ expect(gecen).toEqual([])
318
+ })
319
+
320
+ it.each(AILELER)('%s track alfası düşürülmedi — /20 ayrımı /10\'dan güçlü', (aile) => {
321
+ // Alternatif öneri track'i `/10`'a çekmekti; kabul edilmedi çünkü track
322
+ // zemine yaklaşır ve barın TOPLAM uzunluğu görünmez olur. Altı varyantın
323
+ // ortak `/20` deseni de bozulurdu.
324
+ const zemin = zeminler('light')[0][1]
325
+ const sem = semantikRgb(aile, 'light')
326
+ const yirmi = rgbKontrast(alfaBileske(sem, 0.2, zemin), zemin)
327
+ const on = rgbKontrast(alfaBileske(sem, 0.1, zemin), zemin)
328
+ expect(yirmi).toBeGreaterThan(on)
329
+ })
330
+ })
331
+ })
@@ -235,14 +235,21 @@ describe('Progress Component', () => {
235
235
  render(<Progress indicatorVariant="warning" />)
236
236
  const progress = screen.getByRole('progressbar')
237
237
  const indicator = progress.querySelector('div')
238
- expect(indicator).toHaveClass('bg-warning')
238
+ // #424: dolgu light'ta artık `bg-warning` DEĞİL — o token track'in
239
+ // (`bg-warning/20`) aynısıydı, light'ta ayrım 1.83'e düşüyordu. Dark
240
+ // render'ı korumak için semantik token `dark:` altına taşındı.
241
+ // Gerekçe + kontrast hesabı: progress-warning-error-contrast.test.ts
242
+ expect(indicator).toHaveClass('bg-[rgb(var(--warning-700))]')
243
+ expect(indicator).toHaveClass('dark:bg-warning')
239
244
  })
240
245
 
241
246
  it('renders error indicator variant correctly', () => {
242
247
  render(<Progress indicatorVariant="error" />)
243
248
  const progress = screen.getByRole('progressbar')
244
249
  const indicator = progress.querySelector('div')
245
- expect(indicator).toHaveClass('bg-error')
250
+ // #424: aynı düzeltme — light 2.89 → 4.95, dark render'ı değişmedi.
251
+ expect(indicator).toHaveClass('bg-[rgb(var(--error-700))]')
252
+ expect(indicator).toHaveClass('dark:bg-error')
246
253
  })
247
254
  })
248
255