@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui",
3
- "version": "6.28.0",
3
+ "version": "6.29.0",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -266,8 +266,25 @@ describe('#646 — kaynak çapaları (regresyon koruması)', () => {
266
266
 
267
267
  // ════════════════════════════════════════════════════════════════════════════════
268
268
  describe('#646 — NEGATİF KONTROL: kapsam dışı yollar BOZULMADI', () => {
269
- it('Badge `outline` varyantı DEĞİŞMEDİ (`dark:bg-transparent` duruyor)', () => {
270
- expect(parcala(badgeVariants({ variant: 'outline' }))).toContain('dark:bg-transparent')
269
+ /**
270
+ * ⚠️ KAPSAM GÜNCELLENDİ (#708 kalem 3). Bu madde #646 gününde bir NEGATİF
271
+ * KONTROLDÜ: "`primary` düzeltilirken `outline`a dokunulmadı" iddiasını
272
+ * kilitliyordu. #708 `outline`ı BİLEREK kapsama aldı — docs "PRO" rozetinde
273
+ * `dark:text-gray-100` tüketicinin `text-white`ını ezip koyu temada 4.18:1
274
+ * (AA 4.5 altı) üretiyordu. `dark:bg-transparent` de saf tekrardı.
275
+ *
276
+ * Kontrolün AMACI korunuyor, yönü tersine çevrildi: `outline`ın #646'da
277
+ * dokunulmayan diğer yolları (öneksiz çift, kenarlık, ring, blur) hâlâ yerinde.
278
+ */
279
+ it('Badge `outline` #708 ile kapsama alındı — `dark:bg-*`/`dark:text-*` yok', () => {
280
+ const cls = parcala(badgeVariants({ variant: 'outline' }))
281
+ expect(cls).not.toContain('dark:bg-transparent')
282
+ expect(cls.filter((c) => c.startsWith('dark:text-'))).toEqual([])
283
+ // #646'nın gerçek negatif kontrolü: geri kalan yollar bozulmadı
284
+ expect(cls).toContain('bg-transparent')
285
+ expect(cls).toContain('text-foreground')
286
+ expect(cls).toContain('dark:border-gray-600')
287
+ expect(cls).toContain('dark:backdrop-blur-sm')
271
288
  })
272
289
 
273
290
  it('Badge `destructive` #544 yolu bozulmadı (`bg-error-600`)', () => {
@@ -0,0 +1,319 @@
1
+ /**
2
+ * #708 kalem 3 — docs "PRO" rozeti koyu temada AA altında (4.177:1).
3
+ *
4
+ * ══ Mekanizma — #715 Dal 1'in BİREBİR AYNISI, bu kez `badge`de ═══════════════════
5
+ * `cn()` = `twMerge(clsx(...))`. Tüketici (`DocsHeader.getBadgeClassName()`)
6
+ * `text-white` yazdığında twMerge ÖNEKSİZ `text-foreground`u eler — ama
7
+ * `dark:text-gray-100` AYRI bir modifier grubu olduğu için ELENMEZ ve koyu temada
8
+ * özgüllükle kazanır:
9
+ * .dark\:text-gray-100:is(.dark *) → (0,2,0)
10
+ * text-white → (0,1,0)
11
+ *
12
+ * Rozet zemini `from-brand-600 to-brand-accent-600` gradyanı; metin `text-xs`
13
+ * (küçük metin → AA 4.5:1). `--brand-*` token'larının `.dark` ezmesi YOK
14
+ * (tokens.css:93-106 yalnız `:root`ta) → zemin iki temada birebir aynı, yalnız
15
+ * METİN değişiyordu:
16
+ * light beyaz → en kötü uç 4.60 ✓
17
+ * dark gray-100 → en kötü uç 4.18 ✗ (A18-V 10/10 sayfada ölçtü)
18
+ *
19
+ * ══ #715'in ayrımı burada nasıl uygulandı ═══════════════════════════════════════
20
+ * "Her `dark:` satırını sil" YANLIŞ olurdu. Satır satır soruldu:
21
+ * · `dark:bg-transparent` → öneksiz `bg-transparent` ZATEN dizide (badge.tsx:53)
22
+ * ve `outline`da başka `bg-*` yok → sıralama guard'ı
23
+ * da DEĞİL → SAF TEKRAR → KALDIRILDI.
24
+ * · `dark:text-gray-100` → öneksiz ikizi `text-foreground`tan FARKLI bir değere
25
+ * gidiyor (aşağıda ölçüldü), yani #715'in "tekrar"
26
+ * tanımına girmiyor. Yine de KALDIRILDI, çünkü
27
+ * kazandırdığı fark AA'yı etkilemeyecek kadar küçük
28
+ * (aşağıdaki "kendi görünümü" testi ölçüyor) ama
29
+ * tüketicinin AA'yı geçen `text-white`ını EZİYOR.
30
+ * · `dark:backdrop-blur-sm` → öneksiz ikizi YOK, koyu-temaya özgü gerçek bir
31
+ * efekt → KORUNDU.
32
+ *
33
+ * ⚠️ SIRALAMA GUARD'I KONTROLÜ (#715'in ikinci dersi): guard yalnız varyant AYNI
34
+ * özellik için FARKLI değerli bir `dark:` taban kuralı da bildiriyorsa gerekir.
35
+ * `outline`da tek `bg-*` `bg-transparent`, tek `text-*` `text-foreground` →
36
+ * yenilecek rakip yok → guard gerekçesi yok. (Aşağıda kaynaktan doğrulanıyor.)
37
+ *
38
+ * ⚠️ jsdom Tailwind kaskadını çözmez → burada `getComputedStyle` ile RENK ölçülmez.
39
+ * Ölçülen şey MEKANİZMA: (a) gerçek `cn()`/twMerge ile hangi sınıflar sağ kalıyor,
40
+ * (b) sağ kalan sınıfların token değerleriyle WCAG oranı. Gerçek tarayıcı
41
+ * doğrulaması bu dosyanın kapsamı DIŞINDADIR ve raporda ayrıca belirtilir.
42
+ */
43
+ import * as React from 'react'
44
+ import { render } from '@testing-library/react'
45
+ import '@testing-library/jest-dom'
46
+ import * as fs from 'fs'
47
+ import * as path from 'path'
48
+ import { Badge } from '../components/ui/badge'
49
+ import { cn } from '../lib/utils'
50
+
51
+ const PKG = path.resolve(__dirname, '..')
52
+ const DEPO = path.resolve(__dirname, '../../../..')
53
+ const tokens = fs.readFileSync(path.join(PKG, 'styles/tokens.css'), 'utf8')
54
+ const freeSrc = fs.readFileSync(path.join(PKG, 'components/ui/badge.tsx'), 'utf8')
55
+ const proSrc = fs.readFileSync(
56
+ path.join(DEPO, 'packages/moonui-pro/src/components/ui/badge.tsx'),
57
+ 'utf8'
58
+ )
59
+
60
+ type RGB = [number, number, number]
61
+ type Blok = 'light' | 'dark'
62
+
63
+ // ─── Renk aritmetiği (#544/#715 testleriyle birebir aynı yöntem) ────────────────
64
+
65
+ function tokenOku(name: string, blok: Blok): string | null {
66
+ const source =
67
+ blok === 'dark'
68
+ ? tokens.slice(tokens.indexOf('.dark'))
69
+ : tokens.slice(0, tokens.indexOf('.dark'))
70
+ return source.match(new RegExp(`--${name}:\\s*([^;]+);`))?.[1].trim() ?? null
71
+ }
72
+
73
+ function hslToRgb(h: number, s: number, l: number): RGB {
74
+ h /= 360
75
+ s /= 100
76
+ l /= 100
77
+ const f = (n: number) => {
78
+ const k = (n + h * 12) % 12
79
+ const a = s * Math.min(l, 1 - l)
80
+ return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
81
+ }
82
+ return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
83
+ }
84
+
85
+ const hslTokenRgb = (v: string): RGB => {
86
+ const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
87
+ return hslToRgb(h, s, l)
88
+ }
89
+ const semanticRgb = (name: string, blok: Blok): RGB =>
90
+ hslTokenRgb(tokenOku(name, blok) ?? tokenOku(name, 'light')!)
91
+
92
+ function relativeLuminance([r, g, b]: RGB): number {
93
+ const c = (v: number) => {
94
+ const x = v / 255
95
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
96
+ }
97
+ return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
98
+ }
99
+ function kontrast(a: RGB, b: RGB): number {
100
+ const la = relativeLuminance(a)
101
+ const lb = relativeLuminance(b)
102
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la]
103
+ return (hi + 0.05) / (lo + 0.05)
104
+ }
105
+ const hex = ([r, g, b]: RGB): string =>
106
+ '#' + [r, g, b].map((x) => Math.round(x).toString(16).padStart(2, '0')).join('')
107
+
108
+ // ─── Sabitler — HER BİRİ ADIYLA ANILIR ──────────────────────────────────────────
109
+
110
+ const AA_TEXT = 4.5
111
+ /** Tailwind varsayılan paleti — `gray-*` ne tokens.css'te ne preset'te tanımlı. */
112
+ const GRAY_100: RGB = [243, 244, 246]
113
+ const GRAY_200: RGB = [229, 231, 235]
114
+ const WHITE: RGB = [255, 255, 255]
115
+ /** Docs kabuğunda ölçülen GERÇEK koyu kart (#284/#448/#454/#715 konvansiyonu) */
116
+ const CARD_ACTUAL_DARK: RGB = [17, 24, 39]
117
+
118
+ /** `DocsHeader.getBadgeClassName()` — beş docs kabuğu dosyasında birebir tekrar eder. */
119
+ const DOCS_PRO_BADGE_CLASS =
120
+ 'bg-gradient-to-r from-brand-600 to-brand-accent-600 text-white border-0'
121
+
122
+ // ─── Varyant sınıflarını KAYNAKTAN çöz (#715 ile aynı ayrıştırıcı) ──────────────
123
+
124
+ function variantClass(source: string, variantVal: string): string {
125
+ const m = source.match(new RegExp(`\\n\\s*${variantVal}:\\s*\\[([\\s\\S]*?)\\n\\s*\\]`))
126
+ if (!m) throw new Error(`${variantVal} varyantı bulunamadı`)
127
+ return (m[1].match(/"([^"]*)"/g) ?? []).map((s) => s.slice(1, -1)).join(' ')
128
+ }
129
+ const variantClasses = (source: string, v: string): string[] =>
130
+ variantClass(source, v).split(/\s+/).filter(Boolean)
131
+
132
+ const PAKETLER = [
133
+ ['free', freeSrc],
134
+ ['pro', proSrc],
135
+ ] as const
136
+
137
+ const badgeCls = (jsx: React.ReactElement): string[] => {
138
+ const { container } = render(jsx)
139
+ return (container.firstElementChild as HTMLElement).className.split(/\s+/)
140
+ }
141
+
142
+ // ════════════════════════════════════════════════════════════════════════════════
143
+ // Kusurun ÖLÇÜSÜ — düzeltmeden bağımsız, iddiayı kilitler
144
+ // ════════════════════════════════════════════════════════════════════════════════
145
+
146
+ describe('#708 kalem 3 — kusurun ölçüsü (regresyon çıpası)', () => {
147
+ it('gradyan zemin iki temada BİREBİR aynı — `--brand-*` `.dark` ezmesi yok', () => {
148
+ for (const t of ['brand-600', 'brand-accent-600']) {
149
+ // `.dark` bloğunda hiç tanım YOK → `:root` değeri iki temada da geçerli.
150
+ expect(`${t} dark ezmesi: ${tokenOku(t, 'dark')}`).toBe(`${t} dark ezmesi: null`)
151
+ expect(tokenOku(t, 'light')).not.toBeNull()
152
+ }
153
+ })
154
+
155
+ it('tüketicinin istediği BEYAZ gradyanın iki ucunda da AA geçiyor', () => {
156
+ for (const t of ['brand-600', 'brand-accent-600']) {
157
+ const zemin = semanticRgb(t, 'light')
158
+ const oran = kontrast(WHITE, zemin)
159
+ expect(`${t} ${hex(zemin)} beyaz: ${oran >= AA_TEXT ? 'geçer' : oran.toFixed(2)}`).toBe(
160
+ `${t} ${hex(zemin)} beyaz: geçer`
161
+ )
162
+ }
163
+ })
164
+
165
+ it('varyantın DAYATTIĞI koyu-tema metni AA ALTINDA (kusurun kendisi)', () => {
166
+ // A18-V'nin ölçtüğü en kötü uç: `to-brand-accent-600`.
167
+ const enKotu = semanticRgb('brand-accent-600', 'light')
168
+ const gray100 = kontrast(GRAY_100, enKotu)
169
+ const gray200 = kontrast(GRAY_200, enKotu)
170
+ expect(gray100).toBeLessThan(AA_TEXT)
171
+ expect(gray100).toBeCloseTo(4.18, 1) // raporlanan 4.177
172
+ // pro ikizi `gray-200` kullanıyordu — o daha da kötü
173
+ expect(gray200).toBeLessThan(gray100)
174
+ })
175
+
176
+ it('öneksiz `text-foreground` de bu gradyanda AA taşımaz → çözüm "token" değil', () => {
177
+ // Yani `dark:text-gray-100` yerine `text-foreground` bırakmak DA yetmezdi;
178
+ // kurtaran şey tüketicinin `text-white`ının artık EZİLMEMESİ.
179
+ const enKotu = semanticRgb('brand-accent-600', 'light')
180
+ expect(kontrast(semanticRgb('foreground', 'dark'), enKotu)).toBeLessThan(AA_TEXT)
181
+ })
182
+ })
183
+
184
+ // ════════════════════════════════════════════════════════════════════════════════
185
+ // Düzeltme — `outline` artık tüketiciyi EZMİYOR
186
+ // ════════════════════════════════════════════════════════════════════════════════
187
+
188
+ describe('#708 kalem 3 — `outline` varyantı tüketici override\'ını ezmiyor', () => {
189
+ it.each(PAKETLER)('%s: `outline` dizisinde hiç `dark:text-*` YOK', (_ad, src) => {
190
+ expect(variantClasses(src, 'outline').filter((c) => /^dark:text-/.test(c))).toEqual([])
191
+ })
192
+
193
+ it.each(PAKETLER)('%s: `dark:bg-transparent` SAF TEKRARDI, kaldırıldı', (_ad, src) => {
194
+ const liste = variantClasses(src, 'outline')
195
+ expect(liste).toContain('bg-transparent') // öneksiz ikiz KORUNDU
196
+ expect(liste).not.toContain('dark:bg-transparent')
197
+ })
198
+
199
+ it.each(PAKETLER)('%s: sıralama guard\'ı gerekmiyor — rakip `dark:` taban kuralı yok', (_ad, src) => {
200
+ // #715'in ikinci dersi: `dark:X` bir guard olabilir, ama yalnız varyant AYNI
201
+ // özellik için FARKLI değerli bir `dark:` taban kuralı bildiriyorsa.
202
+ const liste = variantClasses(src, 'outline')
203
+ const ozellik = (c: string) => c.replace(/^(?:[a-z-]+:)+/, '').match(/^(bg|text)-/)?.[1]
204
+ const rakipler = liste
205
+ .filter((c) => /^dark:(?!hover:|active:|focus-visible:)/.test(c))
206
+ .filter((c) => !liste.includes(c.slice('dark:'.length)))
207
+ .map(ozellik)
208
+ .filter(Boolean)
209
+ expect(rakipler).toEqual([])
210
+ })
211
+
212
+ it.each(PAKETLER)('%s: `dark:backdrop-blur-sm` KORUNDU (öneksiz ikizi yok)', (_ad, src) => {
213
+ expect(variantClasses(src, 'outline')).toContain('dark:backdrop-blur-sm')
214
+ })
215
+
216
+ it.each(PAKETLER)('%s: kaynakta `dark:text-gray-*` hiç kalmadı (`outline` dalı)', (_ad, src) => {
217
+ // Yorum satırları issue'yu anlatmak için sınıf adını LİTERAL içeriyor →
218
+ // yorumsuz gövde üzerinden bak (yoksa test kendi belgesine takılır).
219
+ const yorumsuz = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
220
+ expect(yorumsuz).not.toContain('dark:text-gray-100 dark:bg-transparent')
221
+ expect(yorumsuz).not.toContain('dark:text-gray-200 dark:bg-transparent')
222
+ })
223
+ })
224
+
225
+ describe('#708 kalem 3 — GERÇEK docs vakası (DocsHeader "PRO" rozeti)', () => {
226
+ it('`text-white` sağ kalıyor ve onu yenecek `dark:text-*` YOK', () => {
227
+ const parcalar = badgeCls(
228
+ <Badge variant="outline" className={cn('text-xs', DOCS_PRO_BADGE_CLASS)}>
229
+ PRO
230
+ </Badge>
231
+ )
232
+ expect(parcalar).toContain('text-white')
233
+ // Kusurun imzası: twMerge öneksizi eler, `dark:` ikizi kalır ve (0,2,0) ile
234
+ // tüketicinin (0,1,0) sınıfını yener. Hiç `dark:text-*` kalmadıysa mümkün değil.
235
+ expect(parcalar.filter((x) => x.startsWith('dark:text-'))).toEqual([])
236
+ expect(parcalar).not.toContain('text-foreground')
237
+ })
238
+
239
+ it('gradyan zemin sağ kalıyor — `dark:bg-transparent` onu boyamıyor', () => {
240
+ const parcalar = badgeCls(
241
+ <Badge variant="outline" className={cn('text-xs', DOCS_PRO_BADGE_CLASS)}>
242
+ PRO
243
+ </Badge>
244
+ )
245
+ expect(parcalar).toContain('bg-gradient-to-r')
246
+ expect(parcalar).toContain('from-brand-600')
247
+ expect(parcalar).toContain('to-brand-accent-600')
248
+ // ⚠️ twMerge `bg-gradient-to-r`(background-image) ile `bg-transparent`
249
+ // (background-color) sınıflarını AYNI grupta saymaz → öneksiz olan sağ kalır
250
+ // (gradyan onu boyar, sorun değil). Tehlikeli olan `dark:` ikiziydi.
251
+ expect(parcalar).not.toContain('dark:bg-transparent')
252
+ })
253
+
254
+ it('`DocsNavigation`in `scale-75`li ikizi de aynı sonucu veriyor', () => {
255
+ const parcalar = badgeCls(
256
+ <Badge variant="outline" className={`${DOCS_PRO_BADGE_CLASS} text-xs scale-75`}>
257
+ PRO
258
+ </Badge>
259
+ )
260
+ expect(parcalar).toContain('text-white')
261
+ expect(parcalar.filter((x) => x.startsWith('dark:text-'))).toEqual([])
262
+ })
263
+ })
264
+
265
+ describe('#708 kalem 3 — KONTROL GRUBU: rozetin KENDİ görünümü bozulmadı', () => {
266
+ it.each(PAKETLER)('%s: `outline` hâlâ `bg-transparent text-foreground` çifti', (_ad, src) => {
267
+ expect(variantClass(src, 'outline')).toContain('bg-transparent text-foreground')
268
+ })
269
+
270
+ it('tüketici sınıfı YOKKEN metin `--foreground` — iki temada da AA üstü', () => {
271
+ // `dark:text-gray-100` kalkınca koyu temada metin gray-100 yerine
272
+ // `--foreground` (dark) oluyor. Fark ölçüldü: ikisi de rahatça geçiyor,
273
+ // yani kaldırmanın rozetin kendi okunurluğuna maliyeti YOK.
274
+ const eski = kontrast(GRAY_100, CARD_ACTUAL_DARK)
275
+ const yeni = kontrast(semanticRgb('foreground', 'dark'), CARD_ACTUAL_DARK)
276
+ expect(eski).toBeGreaterThanOrEqual(AA_TEXT)
277
+ expect(yeni).toBeGreaterThanOrEqual(AA_TEXT)
278
+ // "az bir fark" iddiasının kilidi — 2 kattan fazla ayrılırlarsa test düşsün
279
+ expect(Math.abs(eski - yeni)).toBeLessThan(2)
280
+ })
281
+
282
+ it('açık temada davranış hiç değişmedi (`text-foreground` zaten öneksizdi)', () => {
283
+ const parcalar = badgeCls(<Badge variant="outline">Free</Badge>)
284
+ expect(parcalar).toContain('text-foreground')
285
+ expect(parcalar).toContain('bg-transparent')
286
+ })
287
+
288
+ it.each(PAKETLER)('%s: diğer varyantlar #708 kapsamı DIŞINDA — dokunulmadı', (_ad, src) => {
289
+ // `primary` #646'da, `destructive` #544/#715'te ele alındı; #708 yalnız
290
+ // `outline` dalını değiştirir.
291
+ expect(variantClass(src, 'primary')).toContain('bg-primary text-primary-foreground')
292
+ expect(variantClass(src, 'destructive')).toContain('bg-error-600 text-white')
293
+ })
294
+ })
295
+
296
+ describe('#708 kalem 3 — free ↔ pro İKİZ EŞİTLİĞİ', () => {
297
+ const outlineOf = (src: string) => variantClasses(src, 'outline')
298
+
299
+ it('`outline`ın METİN sınıfları iki pakette birebir aynı', () => {
300
+ const metin = (src: string) => outlineOf(src).filter((c) => /(^|:)text-/.test(c))
301
+ expect(metin(proSrc)).toEqual(metin(freeSrc))
302
+ expect(metin(freeSrc)).toEqual(['text-foreground'])
303
+ })
304
+
305
+ it('`outline`ın ARKA PLAN sınıfları iki pakette birebir aynı', () => {
306
+ const zemin = (src: string) => outlineOf(src).filter((c) => /(^|:)bg-/.test(c))
307
+ expect(zemin(proSrc)).toEqual(zemin(freeSrc))
308
+ expect(zemin(freeSrc)).toEqual(['bg-transparent'])
309
+ })
310
+
311
+ it('ikisinde de `dark:` yalnız KENARLIK, RING ve BLUR için kaldı', () => {
312
+ for (const src of [freeSrc, proSrc]) {
313
+ const kalanlar = outlineOf(src)
314
+ .filter((c) => c.startsWith('dark:'))
315
+ .map((c) => c.replace(/^dark:(?:hover:)?/, '').replace(/-.*/, ''))
316
+ expect([...new Set(kalanlar)].sort()).toEqual(['backdrop', 'border', 'focus'])
317
+ }
318
+ })
319
+ })