@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.
- package/dist/hooks/index.global.js +1 -6
- package/dist/index.d.mts +44 -35
- package/dist/index.d.ts +44 -35
- package/dist/index.global.js +52 -365
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +421 -359
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +385 -323
- package/dist/index.mjs.map +1 -1
- package/dist/lib/theme.global.js +1 -14
- package/dist/lib/theme.global.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/carousel.test.tsx +37 -0
- 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__/progress.test.tsx +4 -4
- package/src/components/ui/__tests__/quality-group-de.test.tsx +159 -0
- package/src/components/ui/__tests__/radio-group.test.tsx +4 -4
- 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/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/carousel.tsx +24 -0
- 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 +29 -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/scroll-area.tsx +23 -3
- package/src/components/ui/skeleton.tsx +20 -3
- package/src/components/ui/slider.tsx +10 -6
- package/src/components/ui/table.tsx +4 -2
- package/src/components/ui/toggle-group.tsx +4 -1
- 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-
|
|
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,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
|
|
|
@@ -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
|
},
|
|
@@ -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: "
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|