@moontra/moonui 6.9.0 → 6.11.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/hooks/index.global.js +6 -1
- package/dist/hooks/index.global.js.map +1 -1
- package/dist/index.d.mts +34 -33
- package/dist/index.d.ts +34 -33
- package/dist/index.global.js +365 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +121 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +121 -78
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/checkbox.test.tsx +3 -3
- package/src/components/ui/__tests__/collapsible.test.tsx +5 -5
- package/src/components/ui/__tests__/dialog.test.tsx +2 -2
- package/src/components/ui/__tests__/preset-color-shadowing.test.ts +71 -0
- package/src/components/ui/__tests__/progress.test.tsx +4 -4
- package/src/components/ui/__tests__/radio-group.test.tsx +4 -4
- package/src/components/ui/__tests__/semantic-token-usage.test.ts +120 -0
- package/src/components/ui/__tests__/size-scale-convention.test.ts +189 -0
- package/src/components/ui/__tests__/slider.test.tsx +4 -4
- package/src/components/ui/__tests__/toast.test.tsx +6 -3
- package/src/components/ui/__tests__/tooltip.test.tsx +8 -4
- package/src/components/ui/alert.tsx +5 -3
- package/src/components/ui/avatar.tsx +3 -2
- package/src/components/ui/breadcrumb.tsx +4 -2
- package/src/components/ui/card.tsx +4 -2
- package/src/components/ui/checkbox.tsx +5 -3
- package/src/components/ui/collapsible.tsx +10 -6
- package/src/components/ui/color-picker.tsx +11 -5
- package/src/components/ui/command.tsx +4 -2
- package/src/components/ui/date-picker.tsx +4 -2
- package/src/components/ui/dialog.tsx +7 -5
- package/src/components/ui/popover-pro.tsx +5 -3
- package/src/components/ui/popover.tsx +4 -2
- package/src/components/ui/progress.tsx +6 -4
- package/src/components/ui/radio-group.tsx +5 -3
- package/src/components/ui/skeleton.tsx +4 -2
- package/src/components/ui/slider.tsx +10 -6
- package/src/components/ui/table.tsx +4 -2
- package/src/components/ui/toast.tsx +4 -4
- package/src/components/ui/toggle-group.tsx +4 -1
- package/src/components/ui/toggle.tsx +4 -2
- package/src/components/ui/tooltip.tsx +3 -3
- package/src/styles/tokens.css +43 -2
- package/tailwind-preset.js +17 -7
- package/src/use-toast.ts +0 -32
package/package.json
CHANGED
|
@@ -84,13 +84,13 @@ describe('Checkbox Components', () => {
|
|
|
84
84
|
it('renders md size correctly', () => {
|
|
85
85
|
render(<Checkbox size="md" data-testid="checkbox" />)
|
|
86
86
|
const checkbox = screen.getByTestId('checkbox')
|
|
87
|
-
expect(checkbox).toHaveClass('h-
|
|
87
|
+
expect(checkbox).toHaveClass('h-4', 'w-4')
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
it('renders lg size correctly', () => {
|
|
91
91
|
render(<Checkbox size="lg" data-testid="checkbox" />)
|
|
92
92
|
const checkbox = screen.getByTestId('checkbox')
|
|
93
|
-
expect(checkbox).toHaveClass('h-
|
|
93
|
+
expect(checkbox).toHaveClass('h-5', 'w-5')
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
it('uses default size as default', () => {
|
|
@@ -538,7 +538,7 @@ describe('Checkbox Components', () => {
|
|
|
538
538
|
|
|
539
539
|
const checkbox = screen.getByTestId('checkbox')
|
|
540
540
|
expect(checkbox).toHaveClass('bg-transparent')
|
|
541
|
-
expect(checkbox).toHaveClass('h-
|
|
541
|
+
expect(checkbox).toHaveClass('h-5', 'w-5')
|
|
542
542
|
expect(checkbox).toHaveClass('rounded-full')
|
|
543
543
|
expect(checkbox).toHaveClass('transition-all')
|
|
544
544
|
expect(checkbox).toHaveClass('custom-class')
|
|
@@ -197,7 +197,7 @@ describe('Collapsible Components', () => {
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
const trigger = screen.getByTestId('trigger')
|
|
200
|
-
expect(trigger).toHaveClass('text-
|
|
200
|
+
expect(trigger).toHaveClass('text-sm')
|
|
201
201
|
})
|
|
202
202
|
|
|
203
203
|
it('renders lg size correctly', () => {
|
|
@@ -211,7 +211,7 @@ describe('Collapsible Components', () => {
|
|
|
211
211
|
)
|
|
212
212
|
|
|
213
213
|
const trigger = screen.getByTestId('trigger')
|
|
214
|
-
expect(trigger).toHaveClass('text-
|
|
214
|
+
expect(trigger).toHaveClass('text-base')
|
|
215
215
|
})
|
|
216
216
|
|
|
217
217
|
it('renders chevron icon', () => {
|
|
@@ -382,7 +382,7 @@ describe('Collapsible Components', () => {
|
|
|
382
382
|
)
|
|
383
383
|
|
|
384
384
|
const content = screen.getByTestId('content')
|
|
385
|
-
expect(content).toHaveClass('text-
|
|
385
|
+
expect(content).toHaveClass('text-sm')
|
|
386
386
|
})
|
|
387
387
|
|
|
388
388
|
it('renders lg size correctly', () => {
|
|
@@ -396,7 +396,7 @@ describe('Collapsible Components', () => {
|
|
|
396
396
|
)
|
|
397
397
|
|
|
398
398
|
const content = screen.getByTestId('content')
|
|
399
|
-
expect(content).toHaveClass('text-
|
|
399
|
+
expect(content).toHaveClass('text-base')
|
|
400
400
|
})
|
|
401
401
|
|
|
402
402
|
it('renders content wrapper with padding', () => {
|
|
@@ -438,7 +438,7 @@ describe('Collapsible Components', () => {
|
|
|
438
438
|
it('collapsibleContentVariants generates correct classes', () => {
|
|
439
439
|
expect(collapsibleContentVariants()).toContain('overflow-hidden transition-all')
|
|
440
440
|
expect(collapsibleContentVariants({ variant: 'outline' })).toContain('border')
|
|
441
|
-
expect(collapsibleContentVariants({ size: 'lg' })).toContain('text-
|
|
441
|
+
expect(collapsibleContentVariants({ size: 'lg' })).toContain('text-base')
|
|
442
442
|
})
|
|
443
443
|
})
|
|
444
444
|
|
|
@@ -252,7 +252,7 @@ describe('Dialog Components', () => {
|
|
|
252
252
|
</Dialog>
|
|
253
253
|
)
|
|
254
254
|
|
|
255
|
-
expect(screen.getByTestId('content')).toHaveClass('max-w-
|
|
255
|
+
expect(screen.getByTestId('content')).toHaveClass('max-w-lg p-6')
|
|
256
256
|
})
|
|
257
257
|
|
|
258
258
|
it('renders different radius variants', () => {
|
|
@@ -685,7 +685,7 @@ describe('Dialog Components', () => {
|
|
|
685
685
|
const content = screen.getByTestId('dialog-content')
|
|
686
686
|
expect(content).toHaveClass('custom-dialog')
|
|
687
687
|
expect(content).toHaveClass('border-primary/20 dark:border-primary/30')
|
|
688
|
-
expect(content).toHaveClass('max-w-
|
|
688
|
+
expect(content).toHaveClass('max-w-lg p-6')
|
|
689
689
|
expect(content).toHaveClass('rounded-2xl')
|
|
690
690
|
expect(content).toHaveClass('top-[5%]')
|
|
691
691
|
expect(screen.getByTestId('dialog-title')).toHaveTextContent('Advanced Dialog')
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind preset'inde RENK GÖLGELEME bekçisi.
|
|
3
|
+
*
|
|
4
|
+
* Gerçek olay: `theme.extend.borderColor` bloğu `success`/`warning`/`info`/`error`/`caution`'ı
|
|
5
|
+
* DÜZ STRING olarak tanımlıyordu. Tailwind bu düz değeri `theme.colors` altındaki İÇ İÇE
|
|
6
|
+
* nesnenin üzerine yazar → o beş isimde `border-*` ailesi için yalnız DEFAULT hayatta kalır,
|
|
7
|
+
* `border-success-subtle-foreground` gibi alt anahtarlar **hiç CSS üretmez**.
|
|
8
|
+
*
|
|
9
|
+
* Bu sessiz bir kusurdur: TypeScript görmez, jest render testi görmez (sınıf DOM'da vardır,
|
|
10
|
+
* yalnız karşılığı yoktur). #313/#314/#323'te toast'a eklenen üç kenarlık sınıfı tam bu yüzden
|
|
11
|
+
* ölü çıktı ve ancak deterministik bir Tailwind CLI probe'uyla yakalandı.
|
|
12
|
+
*/
|
|
13
|
+
import preset from '../../../../tailwind-preset.js'
|
|
14
|
+
|
|
15
|
+
type ColorValue = string | Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
// DİKKAT: bu preset'te renkler `theme.colors` DEĞİL, `theme.extend.colors` altında.
|
|
18
|
+
// (İlk yazımda yanlış yere baktım ve test kırmızı oldu — yapı varsayılmamalı, okunmalı.)
|
|
19
|
+
const extend = (preset as any).theme?.extend as Record<string, unknown> | undefined
|
|
20
|
+
const colors = extend?.colors as Record<string, ColorValue> | undefined
|
|
21
|
+
|
|
22
|
+
/** `border-*`, `text-*`, `bg-*` … ailelerini besleyen, renk gölgeleyebilen extend anahtarları */
|
|
23
|
+
const RENK_AILELERI = [
|
|
24
|
+
'borderColor',
|
|
25
|
+
'textColor',
|
|
26
|
+
'backgroundColor',
|
|
27
|
+
'ringColor',
|
|
28
|
+
'divideColor',
|
|
29
|
+
'outlineColor',
|
|
30
|
+
'fill',
|
|
31
|
+
'stroke',
|
|
32
|
+
'placeholderColor',
|
|
33
|
+
'accentColor',
|
|
34
|
+
'caretColor',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
describe('tailwind preset — iç içe renk anahtarları gölgelenmiyor', () => {
|
|
38
|
+
it('preset okunabiliyor ve colors tanımlı (sessiz yeşil koruması)', () => {
|
|
39
|
+
expect(colors).toBeDefined()
|
|
40
|
+
// `destructive` iç içe nesne olmalı — tarayıcının canlı olduğunun kanıtı
|
|
41
|
+
expect(typeof colors!.destructive).toBe('object')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('hiçbir renk ailesi extend\'i, iç içe bir `colors` girdisini DÜZ STRING ile ezmiyor', () => {
|
|
45
|
+
const ihlaller: string[] = []
|
|
46
|
+
for (const aile of RENK_AILELERI) {
|
|
47
|
+
const blok = extend?.[aile] as Record<string, ColorValue> | undefined
|
|
48
|
+
if (!blok || typeof blok !== 'object') continue
|
|
49
|
+
for (const [isim, deger] of Object.entries(blok)) {
|
|
50
|
+
const asil = colors?.[isim]
|
|
51
|
+
if (asil && typeof asil === 'object' && typeof deger === 'string') {
|
|
52
|
+
const kayipAnahtarlar = Object.keys(asil).filter((k) => k !== 'DEFAULT')
|
|
53
|
+
ihlaller.push(
|
|
54
|
+
`theme.extend.${aile}.${isim} düz string → colors.${isim}'in ` +
|
|
55
|
+
`[${kayipAnahtarlar.join(', ')}] alt anahtarları ${aile} için ÖLÜ`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
expect(ihlaller).toEqual([])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('semantik renklerin subtle alt anahtarları preset\'te tanımlı', () => {
|
|
64
|
+
for (const isim of ['destructive', 'success', 'warning', 'info']) {
|
|
65
|
+
const c = colors?.[isim] as Record<string, unknown> | undefined
|
|
66
|
+
expect(c).toBeDefined()
|
|
67
|
+
expect(c).toHaveProperty('subtle')
|
|
68
|
+
expect(c).toHaveProperty('subtle-foreground')
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -122,19 +122,19 @@ describe('Progress Component', () => {
|
|
|
122
122
|
it('renders md size correctly', () => {
|
|
123
123
|
render(<Progress size="md" />)
|
|
124
124
|
const progress = screen.getByRole('progressbar')
|
|
125
|
-
expect(progress).toHaveClass('h-2
|
|
125
|
+
expect(progress).toHaveClass('h-2')
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
it('renders lg size correctly', () => {
|
|
129
129
|
render(<Progress size="lg" />)
|
|
130
130
|
const progress = screen.getByRole('progressbar')
|
|
131
|
-
expect(progress).toHaveClass('h-
|
|
131
|
+
expect(progress).toHaveClass('h-2.5')
|
|
132
132
|
})
|
|
133
133
|
|
|
134
134
|
it('renders xl size correctly', () => {
|
|
135
135
|
render(<Progress size="xl" />)
|
|
136
136
|
const progress = screen.getByRole('progressbar')
|
|
137
|
-
expect(progress).toHaveClass('h-
|
|
137
|
+
expect(progress).toHaveClass('h-3')
|
|
138
138
|
})
|
|
139
139
|
})
|
|
140
140
|
|
|
@@ -327,7 +327,7 @@ describe('Progress Component', () => {
|
|
|
327
327
|
|
|
328
328
|
const progress = screen.getByRole('progressbar')
|
|
329
329
|
expect(progress).toHaveClass('bg-success/20')
|
|
330
|
-
expect(progress).toHaveClass('h-
|
|
330
|
+
expect(progress).toHaveClass('h-2.5')
|
|
331
331
|
expect(progress).toHaveClass('rounded-sm')
|
|
332
332
|
expect(progress).toHaveClass('[&>div]:duration-300')
|
|
333
333
|
|
|
@@ -199,7 +199,7 @@ describe('RadioGroup Components', () => {
|
|
|
199
199
|
)
|
|
200
200
|
|
|
201
201
|
const label = screen.getByDisplayValue('test').nextElementSibling
|
|
202
|
-
expect(label).toHaveClass('h-
|
|
202
|
+
expect(label).toHaveClass('h-4', 'w-4')
|
|
203
203
|
})
|
|
204
204
|
|
|
205
205
|
it('renders lg size correctly', () => {
|
|
@@ -210,7 +210,7 @@ describe('RadioGroup Components', () => {
|
|
|
210
210
|
)
|
|
211
211
|
|
|
212
212
|
const label = screen.getByDisplayValue('test').nextElementSibling
|
|
213
|
-
expect(label).toHaveClass('h-
|
|
213
|
+
expect(label).toHaveClass('h-5', 'w-5')
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
it('renders custom indicator when checked', () => {
|
|
@@ -466,8 +466,8 @@ describe('RadioGroup Components', () => {
|
|
|
466
466
|
const filledItem = screen.getByDisplayValue('filled').nextElementSibling
|
|
467
467
|
|
|
468
468
|
expect(defaultItem).toHaveClass('h-3.5', 'w-3.5', 'border-border', 'bg-background')
|
|
469
|
-
expect(outlineItem).toHaveClass('h-
|
|
470
|
-
expect(filledItem).toHaveClass('h-
|
|
469
|
+
expect(outlineItem).toHaveClass('h-4', 'w-4', 'border-border', 'bg-transparent')
|
|
470
|
+
expect(filledItem).toHaveClass('h-5', 'w-5', 'border-primary', 'bg-primary/10')
|
|
471
471
|
})
|
|
472
472
|
|
|
473
473
|
it('handles name attribute for form submission', () => {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #313 / #314 / #323 — semantik durum varyantlarında ham palet bekçisi.
|
|
3
|
+
*
|
|
4
|
+
* Denetimin bulgusu: `destructive`/`success`/`warning`/`info` durum varyantları paketin
|
|
5
|
+
* kendi token'ları yerine ham Tailwind paletine (`bg-red-100 text-red-900`) düşmüştü.
|
|
6
|
+
* Sonuç görsel değil **işlevsel**: marka/tema özelleştirmesi bu varyantlara İŞLEMİYORDU —
|
|
7
|
+
* tüketici `--destructive`'i değiştirdiğinde menü item'ı ve toast eski kırmızıda kalıyordu.
|
|
8
|
+
*
|
|
9
|
+
* Çözüm `--*-subtle` token ailesiydi (düz `--destructive` "dolu zemin + açık metin" içindir;
|
|
10
|
+
* yumuşak tint + koyu metin için karşılığı yoktu). Bu test o dönüşümün geri gelmemesini korur.
|
|
11
|
+
*
|
|
12
|
+
* KAPSAM NOTU: kural yalnızca **durum varyantı** tanımlayan cva bloklarını hedefler.
|
|
13
|
+
* `premium` gibi DEKORATİF aksanlar (altın/gradient) kapsam dışıdır — bir "premium" rozetini
|
|
14
|
+
* `--warning`e bağlamak semantik olarak yanlış olurdu; onlar ayrı bir token ailesi ister.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs'
|
|
17
|
+
import * as path from 'path'
|
|
18
|
+
|
|
19
|
+
/** Ham Tailwind palet ailesi — semantik durumların yerine geçmemeli. */
|
|
20
|
+
const HAM_PALET = /\b(?:bg|text|border|ring|from|to|via)-(red|green|yellow|orange|blue)-\d{2,3}\b/
|
|
21
|
+
|
|
22
|
+
/** Durum varyantı adları — cva içinde bu anahtarların değerleri token kullanmalı. */
|
|
23
|
+
const DURUM_ANAHTARLARI = ['destructive', 'success', 'warning', 'info', 'error']
|
|
24
|
+
|
|
25
|
+
interface Ihlal {
|
|
26
|
+
file: string
|
|
27
|
+
line: number
|
|
28
|
+
anahtar: string
|
|
29
|
+
parca: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function tara(root: string): Ihlal[] {
|
|
33
|
+
const files: string[] = []
|
|
34
|
+
;(function walk(dir: string) {
|
|
35
|
+
if (!fs.existsSync(dir)) return
|
|
36
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
37
|
+
const p = path.join(dir, e.name)
|
|
38
|
+
if (e.isDirectory()) {
|
|
39
|
+
if (!/node_modules|__tests__|dist/.test(p)) walk(p)
|
|
40
|
+
} else if (/\.tsx?$/.test(e.name) && !/\.stories\./.test(e.name)) files.push(p)
|
|
41
|
+
}
|
|
42
|
+
})(root)
|
|
43
|
+
|
|
44
|
+
const ihlaller: Ihlal[] = []
|
|
45
|
+
for (const f of files) {
|
|
46
|
+
const src = fs.readFileSync(f, 'utf8')
|
|
47
|
+
for (const anahtar of DURUM_ANAHTARLARI) {
|
|
48
|
+
// `destructive: "…"` biçimindeki cva varyant girdisini yakala
|
|
49
|
+
const re = new RegExp(`\\n[ \\t]*${anahtar}:\\s*(["'])([\\s\\S]*?)\\1`, 'g')
|
|
50
|
+
for (const m of src.matchAll(re)) {
|
|
51
|
+
const deger = m[2]
|
|
52
|
+
const hit = deger.match(HAM_PALET)
|
|
53
|
+
if (hit) {
|
|
54
|
+
ihlaller.push({
|
|
55
|
+
file: path.relative(process.cwd(), f),
|
|
56
|
+
line: src.slice(0, m.index).split('\n').length + 1,
|
|
57
|
+
anahtar,
|
|
58
|
+
parca: hit[0],
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return ihlaller
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* BİLİNEN KALAN İHLALLER — cırcır (ratchet) listesi.
|
|
69
|
+
*
|
|
70
|
+
* Bu PR (#313/#314/#323) menü + toast ailesini dönüştürdü. Bekçi kurulunca aynı desenin
|
|
71
|
+
* kütüphanede DAHA GENİŞ olduğu ortaya çıktı: 7 dosyada 21 ihlal daha. Onları bu PR'a
|
|
72
|
+
* çekmek kapsamı ilgisiz component'lere (dashboard widget'ları, timeline, avatar-pro…)
|
|
73
|
+
* yayardı; bu yüzden burada AÇIKÇA listeleniyorlar.
|
|
74
|
+
*
|
|
75
|
+
* Kural cırcır gibi çalışır: listedekiler tolere edilir, **yeni** ihlal kırmızı olur.
|
|
76
|
+
* Takip işi bu dosyaları dönüştürüp satırları buradan siler — liste boşalınca kural
|
|
77
|
+
* tam sıkı hale gelir.
|
|
78
|
+
*/
|
|
79
|
+
const BILINEN_KALANLAR: string[] = []
|
|
80
|
+
|
|
81
|
+
const SRC_ROOT = path.resolve(__dirname, '../../..')
|
|
82
|
+
const tumIhlaller = tara(SRC_ROOT)
|
|
83
|
+
const ihlaller = tumIhlaller.filter(
|
|
84
|
+
(i) => !BILINEN_KALANLAR.some((b) => i.file.replace(/\\/g, '/').endsWith(b))
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
describe('#313/#314/#323 semantik durum varyantları token kullanır', () => {
|
|
88
|
+
it('tarayıcı gerçekten dosya buluyor (sessiz yeşil koruması)', () => {
|
|
89
|
+
// Regex/yol bozulursa test "0 ihlal" ile yeşil geçerdi. Bilinen bir durum
|
|
90
|
+
// varyantının var olduğunu doğrulayarak tarayıcının canlı olduğunu kanıtlıyoruz.
|
|
91
|
+
const toast = fs.readFileSync(
|
|
92
|
+
path.join(SRC_ROOT, 'components/ui/toast.tsx'),
|
|
93
|
+
'utf8'
|
|
94
|
+
)
|
|
95
|
+
expect(toast).toContain('success:')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('durum varyantlarında ham Tailwind paleti YOK (bilinen kalanlar hariç)', () => {
|
|
99
|
+
const rapor = ihlaller.map((i) => `${i.file}:${i.line} → ${i.anahtar} içinde "${i.parca}"`)
|
|
100
|
+
expect(rapor).toEqual([])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('cırcır listesi ÇALIŞIYOR — listelenen dosyalar gerçekten hâlâ ihlalli', () => {
|
|
104
|
+
// Bir dosya dönüştürüldüğünde bu test kırmızı olur ve satırı listeden silmeyi hatırlatır.
|
|
105
|
+
// Aksi hâlde liste sonsuza dek şişer ve kural sessizce gevşer.
|
|
106
|
+
const halaIhlalli = new Set(
|
|
107
|
+
tumIhlaller.map((i) => BILINEN_KALANLAR.find((b) => i.file.replace(/\\/g, '/').endsWith(b)))
|
|
108
|
+
)
|
|
109
|
+
const gereksizListelenen = BILINEN_KALANLAR.filter((b) => !halaIhlalli.has(b))
|
|
110
|
+
expect(gereksizListelenen).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('toast success/warning/info gerçekten `--*-subtle` token\'larını kullanıyor', () => {
|
|
114
|
+
const toast = fs.readFileSync(path.join(SRC_ROOT, 'components/ui/toast.tsx'), 'utf8')
|
|
115
|
+
expect(toast).toContain('bg-success-subtle')
|
|
116
|
+
expect(toast).toContain('text-success-subtle-foreground')
|
|
117
|
+
expect(toast).toContain('bg-warning-subtle')
|
|
118
|
+
expect(toast).toContain('bg-info-subtle')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #319 — `size` skalası konvansiyon bekçisi (free paket).
|
|
3
|
+
*
|
|
4
|
+
* Bu test bir component'i değil, **kütüphane genelindeki API tutarlılığını** koruyor.
|
|
5
|
+
* Denetim öncesi durum: 80 `size` varyant haritası, **23 farklı skala**; orta basamak kimi
|
|
6
|
+
* yerde `default`, kimi yerde `md`, **17 haritada İKİSİ BİRDEN ayrı basamak** olarak. Sonuç:
|
|
7
|
+
* `size="md"` bazı component'lerde çalışıyor, bazılarında sessizce varsayılana düşüyordu; ve
|
|
8
|
+
* `default` ile `md` **farklı** şeyler üretiyordu (ör. checkbox 16px vs 20px).
|
|
9
|
+
*
|
|
10
|
+
* Kanonik karar: `xs · sm · md · lg · xl · 2xl · 3xl · 4xl · 5xl`.
|
|
11
|
+
* `default` bir AD değil, bir varsayılan-işaretidir → CVA'da yeri `defaultVariants`.
|
|
12
|
+
* Geri uyum için `default` anahtarı **saf alias** olarak korunuyor (en az bir minor sürüm).
|
|
13
|
+
*
|
|
14
|
+
* Yeni bir component eklerken bu testin kırılması, skalanın sapmaya başladığı anlamına gelir.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs'
|
|
17
|
+
import * as path from 'path'
|
|
18
|
+
|
|
19
|
+
/** Sıralı boyut basamakları — kanonik skala. */
|
|
20
|
+
const CANONICAL_STEPS = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl']
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Boyut basamağı OLMAYAN, ama `size` ekseninde meşru olan özel değerler.
|
|
24
|
+
* Bunlar bir ölçek üzerinde yer tutmaz; ayrı bir davranış seçer.
|
|
25
|
+
*/
|
|
26
|
+
const SPECIAL_VALUES = ['icon', 'full', 'fullscreen', 'auto', 'responsive']
|
|
27
|
+
|
|
28
|
+
/** Geriye dönük uyum için korunan, kaldırılması planlanan alias. */
|
|
29
|
+
const DEPRECATED_ALIAS = 'default'
|
|
30
|
+
|
|
31
|
+
const ALLOWED = new Set([...CANONICAL_STEPS, ...SPECIAL_VALUES, DEPRECATED_ALIAS])
|
|
32
|
+
|
|
33
|
+
interface SizeMap {
|
|
34
|
+
file: string
|
|
35
|
+
line: number
|
|
36
|
+
entries: Array<[string, string]>
|
|
37
|
+
defaultVariant: string | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collectSizeMaps(root: string): SizeMap[] {
|
|
41
|
+
const files: string[] = []
|
|
42
|
+
;(function walk(dir: string) {
|
|
43
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
const p = path.join(dir, e.name)
|
|
45
|
+
if (e.isDirectory()) {
|
|
46
|
+
if (!/node_modules|__tests__|dist/.test(p)) walk(p)
|
|
47
|
+
} else if (/\.tsx?$/.test(e.name) && !/\.stories\./.test(e.name)) {
|
|
48
|
+
files.push(p)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})(root)
|
|
52
|
+
|
|
53
|
+
const maps: SizeMap[] = []
|
|
54
|
+
for (const f of files) {
|
|
55
|
+
const src = fs.readFileSync(f, 'utf8')
|
|
56
|
+
const re = /\bsize:\s*\{/g
|
|
57
|
+
let m: RegExpExecArray | null
|
|
58
|
+
while ((m = re.exec(src))) {
|
|
59
|
+
// dengeli parantezle bloğu çıkar
|
|
60
|
+
let i = src.indexOf('{', m.index)
|
|
61
|
+
let depth = 0
|
|
62
|
+
let end = i
|
|
63
|
+
for (; end < src.length; end++) {
|
|
64
|
+
if (src[end] === '{') depth++
|
|
65
|
+
else if (src[end] === '}') {
|
|
66
|
+
depth--
|
|
67
|
+
if (depth === 0) break
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const body = src.slice(i + 1, end)
|
|
71
|
+
const entries = [
|
|
72
|
+
...body.matchAll(/\n[ \t]*(["']?)([A-Za-z0-9_]+)\1[ \t]*:[ \t]*(["'])([\s\S]*?)\3/g),
|
|
73
|
+
].map((x) => [x[2], x[4].replace(/\s+/g, ' ').trim()] as [string, string])
|
|
74
|
+
if (!entries.length) {
|
|
75
|
+
re.lastIndex = end
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const dvBlock = src.slice(end).match(/defaultVariants:\s*\{([^}]*)\}/)
|
|
79
|
+
const dv = dvBlock?.[1].match(/\bsize\s*:\s*["']([^"']+)["']/)?.[1] ?? null
|
|
80
|
+
maps.push({
|
|
81
|
+
file: path.relative(process.cwd(), f),
|
|
82
|
+
line: src.slice(0, m.index).split('\n').length,
|
|
83
|
+
entries,
|
|
84
|
+
defaultVariant: dv,
|
|
85
|
+
})
|
|
86
|
+
re.lastIndex = end
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return maps
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SRC_ROOT = path.resolve(__dirname, '../../..')
|
|
93
|
+
const maps = collectSizeMaps(SRC_ROOT)
|
|
94
|
+
|
|
95
|
+
describe('#319 size skalası konvansiyonu (free)', () => {
|
|
96
|
+
it('taranacak size haritası buldu (tarayıcının kendisi sessizce boşa düşmesin)', () => {
|
|
97
|
+
// Bu koruma olmadan, regex bozulduğunda tüm testler "0 harita" ile YEŞİL geçerdi.
|
|
98
|
+
expect(maps.length).toBeGreaterThan(20)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('her size anahtarı kanonik sözlükte', () => {
|
|
102
|
+
const sapmalar = maps.flatMap((m) =>
|
|
103
|
+
m.entries
|
|
104
|
+
.map(([k]) => k)
|
|
105
|
+
.filter((k) => !ALLOWED.has(k))
|
|
106
|
+
.map((k) => `${m.file}:${m.line} → "${k}"`)
|
|
107
|
+
)
|
|
108
|
+
expect(sapmalar).toEqual([])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('`default` yalnız SAF ALIAS olarak var — başka bir basamakla aynı değeri taşır', () => {
|
|
112
|
+
const ihlaller = maps
|
|
113
|
+
.filter((m) => m.entries.some(([k]) => k === DEPRECATED_ALIAS))
|
|
114
|
+
.filter((m) => {
|
|
115
|
+
const val = m.entries.find(([k]) => k === DEPRECATED_ALIAS)![1]
|
|
116
|
+
// aynı değeri taşıyan, `default` olmayan en az bir anahtar bulunmalı
|
|
117
|
+
return !m.entries.some(([k, v]) => k !== DEPRECATED_ALIAS && v === val)
|
|
118
|
+
})
|
|
119
|
+
.map((m) => `${m.file}:${m.line}`)
|
|
120
|
+
expect(ihlaller).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('`defaultVariants.size` kanonik bir ada işaret eder, `default`a DEĞİL', () => {
|
|
124
|
+
const ihlaller = maps
|
|
125
|
+
.filter((m) => m.defaultVariant === DEPRECATED_ALIAS)
|
|
126
|
+
.map((m) => `${m.file}:${m.line}`)
|
|
127
|
+
expect(ihlaller).toEqual([])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Denetimin asıl bulgusu buydu: `default` ile `md` **ayrı basamaklar** olduğu için
|
|
132
|
+
* `size="default"` ve `size="md"` farklı şeyler üretiyordu (checkbox 16px vs 20px).
|
|
133
|
+
*
|
|
134
|
+
* Kural `default === md` DEĞİL — dialog'da `md` (max-w-md) varsayılandan küçüktü, orada
|
|
135
|
+
* `default` bilerek `lg`ye eşlendi. Doğru ve daha güçlü kural: `default`, component'in
|
|
136
|
+
* gerçekten varsayılan olarak ürettiği şeyin alias'ı olmalı. Böylece `size="default"`
|
|
137
|
+
* yazan tüketici, prop hiç vermemiş gibi bir sonuç alır.
|
|
138
|
+
*/
|
|
139
|
+
it('`default`, `defaultVariants`ın işaret ettiği basamakla AYNI değeri üretir', () => {
|
|
140
|
+
const ihlaller = maps
|
|
141
|
+
.filter((m) => m.entries.some(([k]) => k === DEPRECATED_ALIAS))
|
|
142
|
+
.filter((m) => {
|
|
143
|
+
const aliasVal = m.entries.find(([k]) => k === DEPRECATED_ALIAS)![1]
|
|
144
|
+
const dvVal = m.entries.find(([k]) => k === m.defaultVariant)?.[1]
|
|
145
|
+
return dvVal === undefined || aliasVal !== dvVal
|
|
146
|
+
})
|
|
147
|
+
.map((m) => `${m.file}:${m.line} (default→"${m.defaultVariant}")`)
|
|
148
|
+
expect(ihlaller).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* CVA DIŞI kaçış yolu. `AdvancedColorPicker` (pro) `size`'ı bir cva varyantı değil, elle
|
|
153
|
+
* yazılmış bir TS union + `sizeClasses` sözlüğü olarak tutuyordu — bu yüzden hem otomatik
|
|
154
|
+
* migrasyondan hem de yukarıdaki cva tabanlı kurallardan KAÇTI ve `"sm" | "default" | "lg"`
|
|
155
|
+
* olarak kaldı. Docs senkronu sırasında yakalandı; bu kural o boşluğu kapatır.
|
|
156
|
+
*/
|
|
157
|
+
it('elle yazılmış `size?:` union tipleri de kanonik skalayı kullanır', () => {
|
|
158
|
+
const files: string[] = []
|
|
159
|
+
;(function walk(dir: string) {
|
|
160
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
161
|
+
const p = path.join(dir, e.name)
|
|
162
|
+
if (e.isDirectory()) {
|
|
163
|
+
if (!/node_modules|__tests__|dist/.test(p)) walk(p)
|
|
164
|
+
} else if (/\.tsx?$/.test(e.name) && !/\.stories\./.test(e.name)) files.push(p)
|
|
165
|
+
}
|
|
166
|
+
})(SRC_ROOT)
|
|
167
|
+
|
|
168
|
+
const ihlaller: string[] = []
|
|
169
|
+
for (const f of files) {
|
|
170
|
+
const src = fs.readFileSync(f, 'utf8')
|
|
171
|
+
for (const m of src.matchAll(/\bsize\??\s*:\s*((?:"[a-zA-Z0-9_]+"\s*\|\s*)+"[a-zA-Z0-9_]+")/g)) {
|
|
172
|
+
const values = [...m[1].matchAll(/"([a-zA-Z0-9_]+)"/g)].map((x) => x[1])
|
|
173
|
+
const yer = `${path.relative(process.cwd(), f)}:${src.slice(0, m.index).split('\n').length}`
|
|
174
|
+
|
|
175
|
+
// (a) sözlük dışı bir değer
|
|
176
|
+
const disi = values.filter((v) => !ALLOWED.has(v))
|
|
177
|
+
if (disi.length) ihlaller.push(`${yer} → sözlük dışı: ${disi.join(', ')}`)
|
|
178
|
+
|
|
179
|
+
// (b) `default` ALIAS'tır: kanonik adın YERİNE geçemez, yanında durur.
|
|
180
|
+
// `"sm" | "default" | "lg"` gibi bir union `md`yi hiç sunmadığı için ihlaldir —
|
|
181
|
+
// color-picker tam olarak bu şekilde migrasyondan kaçmıştı.
|
|
182
|
+
if (values.includes(DEPRECATED_ALIAS) && !values.includes('md')) {
|
|
183
|
+
ihlaller.push(`${yer} → \`default\` var ama kanonik \`md\` YOK`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
expect(ihlaller).toEqual([])
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -53,14 +53,14 @@ describe('Slider Component', () => {
|
|
|
53
53
|
render(<Slider size="md" data-testid="slider" />)
|
|
54
54
|
|
|
55
55
|
const sliderContainer = screen.getByTestId('slider').children[0] as HTMLElement
|
|
56
|
-
expect(sliderContainer).toHaveClass('h-
|
|
56
|
+
expect(sliderContainer).toHaveClass('h-6')
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
it('renders lg size correctly', () => {
|
|
60
60
|
render(<Slider size="lg" data-testid="slider" />)
|
|
61
61
|
|
|
62
62
|
const sliderContainer = screen.getByTestId('slider').children[0] as HTMLElement
|
|
63
|
-
expect(sliderContainer).toHaveClass('h-
|
|
63
|
+
expect(sliderContainer).toHaveClass('h-8')
|
|
64
64
|
})
|
|
65
65
|
})
|
|
66
66
|
|
|
@@ -180,14 +180,14 @@ describe('Slider Component', () => {
|
|
|
180
180
|
render(<Slider thumbSize="md" data-testid="slider" />)
|
|
181
181
|
|
|
182
182
|
const thumb = screen.getByRole('slider')
|
|
183
|
-
expect(thumb).toHaveClass('h-
|
|
183
|
+
expect(thumb).toHaveClass('h-4', 'w-4')
|
|
184
184
|
})
|
|
185
185
|
|
|
186
186
|
it('renders lg thumb size correctly', () => {
|
|
187
187
|
render(<Slider thumbSize="lg" data-testid="slider" />)
|
|
188
188
|
|
|
189
189
|
const thumb = screen.getByRole('slider')
|
|
190
|
-
expect(thumb).toHaveClass('h-
|
|
190
|
+
expect(thumb).toHaveClass('h-5', 'w-5')
|
|
191
191
|
})
|
|
192
192
|
})
|
|
193
193
|
|
|
@@ -146,9 +146,12 @@ describe('Toast Components', () => {
|
|
|
146
146
|
const variants = [
|
|
147
147
|
{ variant: 'default', classes: ['bg-background', 'text-foreground'] },
|
|
148
148
|
{ variant: 'destructive', classes: ['bg-destructive', 'text-destructive-foreground'] },
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
// #323: bu üç varyant ham Tailwind paleti kullanıyordu (`bg-green-50` vb.) →
|
|
150
|
+
// marka/tema özelleştirmesi onlara İŞLEMİYORDU. Artık `--*-subtle` semantik
|
|
151
|
+
// token'ları. Kontrast korundu (8.70 / 8.83 / 9.52 — ölçüldü, bkz. changelog).
|
|
152
|
+
{ variant: 'success', classes: ['bg-success-subtle', 'text-success-subtle-foreground'] },
|
|
153
|
+
{ variant: 'warning', classes: ['bg-warning-subtle', 'text-warning-subtle-foreground'] },
|
|
154
|
+
{ variant: 'info', classes: ['bg-info-subtle', 'text-info-subtle-foreground'] },
|
|
152
155
|
] as const
|
|
153
156
|
|
|
154
157
|
variants.forEach(({ variant, classes }) => {
|
|
@@ -156,10 +156,14 @@ describe('Tooltip Components', () => {
|
|
|
156
156
|
const variants = [
|
|
157
157
|
{ variant: 'default', className: 'bg-primary', textClassName: 'text-primary-foreground' },
|
|
158
158
|
{ variant: 'secondary', className: 'bg-secondary', textClassName: 'text-secondary-foreground' },
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
// #313/#314/#323: bu üçü ham paletti ve KONTRASTI FELAKETTİ — ölçüldü:
|
|
160
|
+
// bg-green-500 + text-white = 2.28, bg-yellow-500 + text-white = 1.92 (AA 4.5).
|
|
161
|
+
// Semantik token'a geçince 4.67 / 8.18 oldu. Test eski (kırık) değerleri
|
|
162
|
+
// sabitliyordu; artık token çiftini doğruluyor.
|
|
163
|
+
{ variant: 'success', className: 'bg-success', textClassName: 'text-success-foreground' },
|
|
164
|
+
{ variant: 'warning', className: 'bg-warning', textClassName: 'text-warning-foreground' },
|
|
161
165
|
{ variant: 'destructive', className: 'bg-destructive', textClassName: 'text-destructive-foreground' },
|
|
162
|
-
{ variant: 'info', className: 'bg-
|
|
166
|
+
{ variant: 'info', className: 'bg-info', textClassName: 'text-info-foreground' },
|
|
163
167
|
] as const
|
|
164
168
|
|
|
165
169
|
variants.forEach(({ variant, className, textClassName }) => {
|
|
@@ -659,7 +663,7 @@ describe('Tooltip Components', () => {
|
|
|
659
663
|
|
|
660
664
|
const tooltip = getTooltipContent()
|
|
661
665
|
expect(tooltip).toBeInTheDocument()
|
|
662
|
-
expect(tooltip).toHaveClass('bg-
|
|
666
|
+
expect(tooltip).toHaveClass('bg-success')
|
|
663
667
|
expect(tooltip).toHaveClass('text-base')
|
|
664
668
|
expect(tooltip).toHaveClass('custom-tooltip')
|
|
665
669
|
expect(tooltip).toHaveAttribute('data-side', 'bottom')
|
|
@@ -30,9 +30,11 @@ const alertVariants = cva(
|
|
|
30
30
|
},
|
|
31
31
|
size: {
|
|
32
32
|
sm: "py-2 text-xs",
|
|
33
|
-
|
|
33
|
+
md: "py-3 text-sm",
|
|
34
34
|
lg: "py-4 text-base",
|
|
35
|
-
|
|
35
|
+
/** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
|
|
36
|
+
default: "py-3 text-sm",
|
|
37
|
+
},
|
|
36
38
|
radius: {
|
|
37
39
|
none: "rounded-none",
|
|
38
40
|
sm: "rounded-sm",
|
|
@@ -46,7 +48,7 @@ const alertVariants = cva(
|
|
|
46
48
|
},
|
|
47
49
|
defaultVariants: {
|
|
48
50
|
variant: "default",
|
|
49
|
-
size: "
|
|
51
|
+
size: "md",
|
|
50
52
|
radius: "default",
|
|
51
53
|
withClose: false,
|
|
52
54
|
},
|