@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.
Files changed (46) hide show
  1. package/dist/hooks/index.global.js +6 -1
  2. package/dist/hooks/index.global.js.map +1 -1
  3. package/dist/index.d.mts +34 -33
  4. package/dist/index.d.ts +34 -33
  5. package/dist/index.global.js +365 -52
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +121 -78
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +121 -78
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +1 -1
  12. package/src/components/ui/__tests__/checkbox.test.tsx +3 -3
  13. package/src/components/ui/__tests__/collapsible.test.tsx +5 -5
  14. package/src/components/ui/__tests__/dialog.test.tsx +2 -2
  15. package/src/components/ui/__tests__/preset-color-shadowing.test.ts +71 -0
  16. package/src/components/ui/__tests__/progress.test.tsx +4 -4
  17. package/src/components/ui/__tests__/radio-group.test.tsx +4 -4
  18. package/src/components/ui/__tests__/semantic-token-usage.test.ts +120 -0
  19. package/src/components/ui/__tests__/size-scale-convention.test.ts +189 -0
  20. package/src/components/ui/__tests__/slider.test.tsx +4 -4
  21. package/src/components/ui/__tests__/toast.test.tsx +6 -3
  22. package/src/components/ui/__tests__/tooltip.test.tsx +8 -4
  23. package/src/components/ui/alert.tsx +5 -3
  24. package/src/components/ui/avatar.tsx +3 -2
  25. package/src/components/ui/breadcrumb.tsx +4 -2
  26. package/src/components/ui/card.tsx +4 -2
  27. package/src/components/ui/checkbox.tsx +5 -3
  28. package/src/components/ui/collapsible.tsx +10 -6
  29. package/src/components/ui/color-picker.tsx +11 -5
  30. package/src/components/ui/command.tsx +4 -2
  31. package/src/components/ui/date-picker.tsx +4 -2
  32. package/src/components/ui/dialog.tsx +7 -5
  33. package/src/components/ui/popover-pro.tsx +5 -3
  34. package/src/components/ui/popover.tsx +4 -2
  35. package/src/components/ui/progress.tsx +6 -4
  36. package/src/components/ui/radio-group.tsx +5 -3
  37. package/src/components/ui/skeleton.tsx +4 -2
  38. package/src/components/ui/slider.tsx +10 -6
  39. package/src/components/ui/table.tsx +4 -2
  40. package/src/components/ui/toast.tsx +4 -4
  41. package/src/components/ui/toggle-group.tsx +4 -1
  42. package/src/components/ui/toggle.tsx +4 -2
  43. package/src/components/ui/tooltip.tsx +3 -3
  44. package/src/styles/tokens.css +43 -2
  45. package/tailwind-preset.js +17 -7
  46. package/src/use-toast.ts +0 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui",
3
- "version": "6.9.0",
3
+ "version": "6.11.0",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -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-5', 'w-5')
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-6', 'w-6')
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-6', 'w-6')
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-base')
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-lg')
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-base')
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-lg')
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-lg')
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-2xl p-7')
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-2xl p-7')
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.5')
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-3')
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-4')
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-3')
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-5', 'w-5')
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-6', 'w-6')
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-5', 'w-5', 'border-border', 'bg-transparent')
470
- expect(filledItem).toHaveClass('h-6', 'w-6', 'border-primary', 'bg-primary/10')
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-8')
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-10')
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-5', 'w-5')
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-6', 'w-6')
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
- { variant: 'success', classes: ['bg-green-50', 'text-green-900', 'border-green-200'] },
150
- { variant: 'warning', classes: ['bg-orange-50', 'text-orange-900', 'border-orange-200'] },
151
- { variant: 'info', classes: ['bg-blue-50', 'text-blue-900', 'border-blue-200'] },
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
- { variant: 'success', className: 'bg-green-500', textClassName: 'text-white' },
160
- { variant: 'warning', className: 'bg-yellow-500', textClassName: 'text-white' },
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-blue-500', textClassName: 'text-white' },
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-green-500')
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
- default: "py-3 text-sm",
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: "default",
51
+ size: "md",
50
52
  radius: "default",
51
53
  withClose: false,
52
54
  },