@moontra/moonui 6.8.0 → 6.10.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 (42) hide show
  1. package/dist/hooks/index.global.js +1 -6
  2. package/dist/index.d.mts +44 -35
  3. package/dist/index.d.ts +44 -35
  4. package/dist/index.global.js +52 -365
  5. package/dist/index.global.js.map +1 -1
  6. package/dist/index.js +421 -359
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +385 -323
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/lib/theme.global.js +1 -14
  11. package/dist/lib/theme.global.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/components/ui/__tests__/carousel.test.tsx +37 -0
  14. package/src/components/ui/__tests__/checkbox.test.tsx +3 -3
  15. package/src/components/ui/__tests__/collapsible.test.tsx +5 -5
  16. package/src/components/ui/__tests__/dialog.test.tsx +2 -2
  17. package/src/components/ui/__tests__/progress.test.tsx +4 -4
  18. package/src/components/ui/__tests__/quality-group-de.test.tsx +159 -0
  19. package/src/components/ui/__tests__/radio-group.test.tsx +4 -4
  20. package/src/components/ui/__tests__/size-scale-convention.test.ts +189 -0
  21. package/src/components/ui/__tests__/slider.test.tsx +4 -4
  22. package/src/components/ui/alert.tsx +5 -3
  23. package/src/components/ui/avatar.tsx +3 -2
  24. package/src/components/ui/breadcrumb.tsx +4 -2
  25. package/src/components/ui/card.tsx +4 -2
  26. package/src/components/ui/carousel.tsx +24 -0
  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 +29 -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/scroll-area.tsx +23 -3
  38. package/src/components/ui/skeleton.tsx +20 -3
  39. package/src/components/ui/slider.tsx +10 -6
  40. package/src/components/ui/table.tsx +4 -2
  41. package/src/components/ui/toggle-group.tsx +4 -1
  42. package/src/components/ui/toggle.tsx +4 -2
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Grup D+E — #320 skeleton, #308 bundle (free tarafı).
3
+ *
4
+ * Her test, düzeltme geri alındığında kırmızıya dönecek biçimde yazıldı; canary ile
5
+ * doğrulandı (bkz. changelog).
6
+ */
7
+ import React from 'react'
8
+ import { render, screen } from '@testing-library/react'
9
+ import '@testing-library/jest-dom'
10
+
11
+ import { Skeleton, SkeletonText } from '../skeleton'
12
+ import { ScrollArea } from '../scroll-area'
13
+ import {
14
+ Command,
15
+ CommandInput,
16
+ CommandList,
17
+ CommandGroup,
18
+ CommandItem,
19
+ CommandSeparator,
20
+ } from '../command'
21
+ import {
22
+ Carousel,
23
+ CarouselContent,
24
+ CarouselItem,
25
+ } from '../carousel'
26
+
27
+ // cmdk seçili öğeyi görünür alana kaydırır; jsdom `scrollIntoView` implement etmez.
28
+ // Ortam eksiği — test edilen davranışla ilgisi yok.
29
+ beforeAll(() => {
30
+ if (!Element.prototype.scrollIntoView) {
31
+ Element.prototype.scrollIntoView = function scrollIntoView() {}
32
+ }
33
+ })
34
+
35
+ describe('#320 skeleton — a11y konvansiyonu', () => {
36
+ it('yer tutucu erişilebilirlik ağacından çıkarılır', () => {
37
+ const { container } = render(<Skeleton data-testid="sk" />)
38
+ expect(container.querySelector('[data-testid="sk"]')).toHaveAttribute('aria-hidden', 'true')
39
+ })
40
+
41
+ it('N skeleton, N canlı bölge ÜRETMEZ (role="status" seçilmemesinin gerekçesi)', () => {
42
+ const { container } = render(<SkeletonText lines={5} />)
43
+ // Tek yükleme durumu onlarca skeleton basar; her biri canlı bölge olsaydı ekran
44
+ // okuyucu aynı olayı N kez duyururdu. Duyurunun sahibi spinner'dır.
45
+ expect(container.querySelectorAll('[role="status"]')).toHaveLength(0)
46
+ expect(container.querySelectorAll('[aria-live]')).toHaveLength(0)
47
+ })
48
+
49
+ it('isLoaded && children dalı GERÇEK içerik → gizlenmez', () => {
50
+ render(
51
+ <Skeleton isLoaded data-testid="loaded">
52
+ <span>gerçek içerik</span>
53
+ </Skeleton>
54
+ )
55
+ const el = screen.getByTestId('loaded')
56
+ expect(el).not.toHaveAttribute('aria-hidden')
57
+ expect(screen.getByText('gerçek içerik')).toBeInTheDocument()
58
+ })
59
+ })
60
+
61
+ describe('#308-1 scroll-area — klavye erişimi', () => {
62
+ it('viewport varsayılan olarak odaklanabilir (WCAG 2.1.1)', () => {
63
+ const { container } = render(
64
+ <ScrollArea className="h-20">
65
+ <p>odaklanabilir eleman içermeyen içerik</p>
66
+ </ScrollArea>
67
+ )
68
+ const viewport = container.querySelector('[data-radix-scroll-area-viewport]')
69
+ expect(viewport).not.toBeNull()
70
+ expect(viewport).toHaveAttribute('tabindex', '0')
71
+ })
72
+
73
+ it('focusable={false} ile sekme durağı kaldırılabilir', () => {
74
+ const { container } = render(
75
+ <ScrollArea focusable={false} className="h-20">
76
+ <p>içerik</p>
77
+ </ScrollArea>
78
+ )
79
+ expect(container.querySelector('[data-radix-scroll-area-viewport]'))
80
+ .not.toHaveAttribute('tabindex')
81
+ })
82
+ })
83
+
84
+ describe('#308-2 carousel — slayt duyurusu', () => {
85
+ it('aria-live durum bölgesi var ve atomik', () => {
86
+ const { container } = render(
87
+ <Carousel>
88
+ <CarouselContent>
89
+ <CarouselItem>1</CarouselItem>
90
+ <CarouselItem>2</CarouselItem>
91
+ </CarouselContent>
92
+ </Carousel>
93
+ )
94
+ const live = container.querySelector('[aria-live="polite"]')
95
+ expect(live).not.toBeNull()
96
+ expect(live).toHaveAttribute('aria-atomic', 'true')
97
+ expect(live).toHaveClass('sr-only')
98
+ })
99
+
100
+ /**
101
+ * Duyuru METNİNİN biçimi (`"Slide 2 of 5"`) burada DOĞRULANMIYOR — kasıtlı.
102
+ * jsdom'da her eleman sıfır boyutlu olduğu için embla'nın `scrollSnapList()` sonucu
103
+ * güvenilmez: tek item'lı carousel'de 1 snap, üç item'lıda 0 snap raporlandı. Bu sayıya
104
+ * dayanan bir assert, ürünü değil jsdom'un düzen eksikliğini sabitlerdi.
105
+ * Biçim ve slayt değişiminde güncellenme GERÇEK TARAYICIDA doğrulandı (bkz. changelog).
106
+ */
107
+ it('slayt sayısı BİLİNMİYORKEN boş duyuru basmaz', () => {
108
+ // Embla init olmadan (`total === 0`) metin boş kalmalı — aksi hâlde ekran okuyucuya
109
+ // "Slide 0 of 0" gider. Slayt İÇERMEYEN carousel bu durumu doğrudan üretir.
110
+ const { container } = render(
111
+ <Carousel>
112
+ <CarouselContent />
113
+ </Carousel>
114
+ )
115
+ expect(container.querySelector('[aria-live="polite"]')!.textContent).toBe('')
116
+ })
117
+ })
118
+
119
+ describe('#308-5/6 command', () => {
120
+ it('separator erişilebilirlik ağacından çıkarılır (listbox çocuk kuralı)', () => {
121
+ const { container } = render(
122
+ <Command>
123
+ <CommandInput placeholder="ara" />
124
+ <CommandList>
125
+ <CommandGroup heading="A">
126
+ <CommandItem value="bir">Bir</CommandItem>
127
+ </CommandGroup>
128
+ <CommandSeparator data-testid="sep" alwaysRender />
129
+ <CommandGroup heading="B">
130
+ <CommandItem value="iki">İki</CommandItem>
131
+ </CommandGroup>
132
+ </CommandList>
133
+ </Command>
134
+ )
135
+
136
+ const sep = container.querySelector('[data-testid="sep"]')
137
+ expect(sep).not.toBeNull()
138
+ // cmdk `role="separator"` basar ve prop'lardan SONRA yazdığı için ezilemez;
139
+ // `aria-hidden` ile ağaçtan çıkarılır → listbox'ın sahiplendiği çocuk olmaktan çıkar.
140
+ expect(sep).toHaveAttribute('aria-hidden', 'true')
141
+ })
142
+
143
+ it('listbox\'ın gizlenmemiş çocuğu separator DEĞİL', () => {
144
+ const { container } = render(
145
+ <Command>
146
+ <CommandList>
147
+ <CommandGroup heading="A">
148
+ <CommandItem value="bir">Bir</CommandItem>
149
+ </CommandGroup>
150
+ <CommandSeparator alwaysRender />
151
+ </CommandList>
152
+ </Command>
153
+ )
154
+ const listbox = container.querySelector('[role="listbox"]')!
155
+ const gorunurCocuklar = Array.from(listbox.querySelectorAll('[role="separator"]'))
156
+ .filter((el) => el.getAttribute('aria-hidden') !== 'true')
157
+ expect(gorunurCocuklar).toHaveLength(0)
158
+ })
159
+ })
@@ -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,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
 
@@ -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
  },
@@ -11,13 +11,14 @@ const avatarVariants = cva(
11
11
  {
12
12
  variants: {
13
13
  size: {
14
- default: "h-10 w-10",
15
14
  xs: "h-6 w-6",
16
15
  sm: "h-8 w-8",
17
16
  md: "h-10 w-10",
18
17
  lg: "h-12 w-12",
19
18
  xl: "h-16 w-16",
20
19
  "2xl": "h-20 w-20",
20
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
21
+ default: "h-10 w-10",
21
22
  },
22
23
  radius: {
23
24
  default: "rounded-full",
@@ -34,7 +35,7 @@ const avatarVariants = cva(
34
35
  }
35
36
  },
36
37
  defaultVariants: {
37
- size: "default",
38
+ size: "md",
38
39
  radius: "default",
39
40
  variant: "default"
40
41
  },
@@ -17,14 +17,16 @@ const breadcrumbVariants = cva(
17
17
  ghost: "text-foreground/60 transition-colors",
18
18
  },
19
19
  size: {
20
- default: "text-sm",
20
+ md: "text-sm",
21
21
  sm: "text-xs",
22
22
  lg: "text-base",
23
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
24
+ default: "text-sm",
23
25
  },
24
26
  },
25
27
  defaultVariants: {
26
28
  variant: "default",
27
- size: "default",
29
+ size: "md",
28
30
  },
29
31
  }
30
32
  );
@@ -35,10 +35,12 @@ const cardVariants = cva(
35
35
  glass: "border border-white/20 bg-white/10 backdrop-blur-md dark:border-gray-700/30 dark:bg-gray-800/10",
36
36
  },
37
37
  size: {
38
- default: "",
38
+ md: "",
39
39
  sm: "",
40
40
  lg: "",
41
41
  xl: "",
42
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
43
+ default: "",
42
44
  },
43
45
  radius: {
44
46
  default: "rounded-lg",
@@ -54,7 +56,7 @@ const cardVariants = cva(
54
56
  },
55
57
  defaultVariants: {
56
58
  variant: "default",
57
- size: "default",
59
+ size: "md",
58
60
  radius: "default",
59
61
  },
60
62
  }
@@ -89,12 +89,25 @@ const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
89
89
  );
90
90
  const [canScrollPrev, setCanScrollPrev] = React.useState(false);
91
91
  const [canScrollNext, setCanScrollNext] = React.useState(false);
92
+ /**
93
+ * #308-2: `aria-roledescription="carousel"/"slide"` doğru set edilmişti ama slayt
94
+ * DEĞİŞİMİNİ duyuran hiçbir şey yoktu — klavye/buton ile ilerleyen ekran-okuyucu
95
+ * kullanıcısı hangi slaytta olduğunu öğrenemiyordu. Embla'nın zaten dinlenen "select"
96
+ * olayına bağlı bir `aria-live="polite"` durum bölgesi eklendi.
97
+ */
98
+ const [slideState, setSlideState] = React.useState<{ current: number; total: number }>(
99
+ { current: 0, total: 0 }
100
+ );
92
101
 
93
102
  // Embla "select" olayında ileri/geri butonlarının durumunu güncelle
94
103
  const onSelect = React.useCallback((emblaApi: CarouselApi) => {
95
104
  if (!emblaApi) return;
96
105
  setCanScrollPrev(emblaApi.canScrollPrev());
97
106
  setCanScrollNext(emblaApi.canScrollNext());
107
+ setSlideState({
108
+ current: emblaApi.selectedScrollSnap() + 1,
109
+ total: emblaApi.scrollSnapList().length,
110
+ });
98
111
  }, []);
99
112
 
100
113
  const scrollPrev = React.useCallback(() => {
@@ -164,6 +177,17 @@ const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
164
177
  {...props}
165
178
  >
166
179
  {children}
180
+ {/*
181
+ #308-2: Görsel olarak gizli durum bölgesi. `aria-live="polite"` sayesinde slayt
182
+ değişimi (buton, klavye ya da sürükleme fark etmeksizin) duyurulur; `aria-atomic`
183
+ metnin tamamının okunmasını sağlar. Slayt sayısı henüz bilinmiyorken (ilk render,
184
+ embla init olmadan) hiçbir şey basılmaz — boş duyuru üretmemek için.
185
+ */}
186
+ <div aria-live="polite" aria-atomic="true" className="sr-only">
187
+ {slideState.total > 0
188
+ ? `Slide ${slideState.current} of ${slideState.total}`
189
+ : ""}
190
+ </div>
167
191
  </div>
168
192
  </CarouselContext.Provider>
169
193
  );
@@ -26,9 +26,11 @@ const checkboxVariants = cva(
26
26
  },
27
27
  size: {
28
28
  sm: "h-3.5 w-3.5",
29
+ md: "h-4 w-4",
30
+ lg: "h-5 w-5",
31
+ xl: "h-6 w-6",
32
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
29
33
  default: "h-4 w-4",
30
- md: "h-5 w-5",
31
- lg: "h-6 w-6",
32
34
  },
33
35
  radius: {
34
36
  none: "rounded-none",
@@ -46,7 +48,7 @@ const checkboxVariants = cva(
46
48
  },
47
49
  defaultVariants: {
48
50
  variant: "default",
49
- size: "default",
51
+ size: "md",
50
52
  radius: "default",
51
53
  animation: "default",
52
54
  },
@@ -27,14 +27,16 @@ const collapsibleTriggerVariants = cva(
27
27
  },
28
28
  size: {
29
29
  sm: "text-xs py-2 px-3",
30
+ md: "text-sm py-3 px-4",
31
+ lg: "text-base py-4 px-4",
32
+ xl: "text-lg py-5 px-5",
33
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
30
34
  default: "text-sm py-3 px-4",
31
- md: "text-base py-4 px-4",
32
- lg: "text-lg py-5 px-5",
33
35
  },
34
36
  },
35
37
  defaultVariants: {
36
38
  variant: "default",
37
- size: "default",
39
+ size: "md",
38
40
  },
39
41
  }
40
42
  )
@@ -85,14 +87,16 @@ const collapsibleContentVariants = cva(
85
87
  },
86
88
  size: {
87
89
  sm: "text-xs",
90
+ md: "text-sm",
91
+ lg: "text-base",
92
+ xl: "text-lg",
93
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
88
94
  default: "text-sm",
89
- md: "text-base",
90
- lg: "text-lg",
91
95
  },
92
96
  },
93
97
  defaultVariants: {
94
98
  variant: "default",
95
- size: "default",
99
+ size: "md",
96
100
  },
97
101
  }
98
102
  )
@@ -27,10 +27,12 @@ const colorPickerVariants = cva(
27
27
  {
28
28
  variants: {
29
29
  size: {
30
- default: "h-10 w-10",
30
+ md: "h-10 w-10",
31
31
  sm: "h-8 w-8",
32
32
  lg: "h-12 w-12",
33
33
  xl: "h-16 w-16",
34
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
35
+ default: "h-10 w-10",
34
36
  },
35
37
  shape: {
36
38
  square: "rounded-md",
@@ -39,7 +41,7 @@ const colorPickerVariants = cva(
39
41
  },
40
42
  },
41
43
  defaultVariants: {
42
- size: "default",
44
+ size: "md",
43
45
  shape: "square",
44
46
  },
45
47
  }
@@ -563,7 +565,8 @@ export interface SimpleColorPickerProps {
563
565
  onChange?: (color: string) => void;
564
566
  colors?: string[];
565
567
  className?: string;
566
- size?: "sm" | "default" | "lg";
568
+ /** #319: kanonik skala; `default` `md`nin deprecated alias'ıdır. */
569
+ size?: "sm" | "md" | "lg" | "default";
567
570
  }
568
571
 
569
572
  export function SimpleColorPicker({
@@ -571,12 +574,15 @@ export function SimpleColorPicker({
571
574
  onChange,
572
575
  colors = defaultPresets.slice(0, 10),
573
576
  className,
574
- size = "default",
577
+ // #319: varsayılan kanonik ada çekildi (aynı CSS: h-8 w-8)
578
+ size = "md",
575
579
  }: SimpleColorPickerProps) {
576
580
  const sizeClasses = {
577
581
  sm: "h-6 w-6",
578
- default: "h-8 w-8",
582
+ md: "h-8 w-8",
579
583
  lg: "h-10 w-10",
584
+ /** @deprecated `md` kullanın — #319 kanonik skala; aynı çıktıyı üretir. */
585
+ default: "h-8 w-8",
580
586
  };
581
587
 
582
588
  return (