@moontra/moonui 6.24.0 → 6.26.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/cdn/moonui.css +2 -2
- package/dist/cdn/moonui.esm.js +12 -12
- package/dist/cdn/moonui.global.js +12 -12
- package/dist/index.d.mts +24 -6
- package/dist/index.d.ts +24 -6
- package/dist/index.js +436 -93
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +437 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/issue-544-kontrast.test.tsx +153 -145
- package/src/__tests__/issue-645-overlay-tab-focus.test.tsx +262 -0
- package/src/__tests__/issue-646-dark-scope-override.test.tsx +297 -0
- package/src/__tests__/issue-661-a11y.test.tsx +113 -0
- package/src/__tests__/issue-661-token-kontrast.test.tsx +82 -0
- package/src/components/ui/__tests__/accent-foreground-contrast.test.ts +197 -0
- package/src/components/ui/__tests__/accordion.test.tsx +6 -4
- package/src/components/ui/__tests__/alert.test.tsx +4 -1
- package/src/components/ui/__tests__/button.test.tsx +6 -1
- package/src/components/ui/__tests__/dark-token-contrast.test.ts +18 -18
- package/src/components/ui/__tests__/heading-level-policy.test.tsx +4 -4
- package/src/components/ui/__tests__/issue-643-card-token-parite.test.tsx +113 -0
- package/src/components/ui/__tests__/issue-649-button-aschild.test.tsx +199 -0
- package/src/components/ui/__tests__/issue-664-separator-dekoratif-rol.test.tsx +41 -0
- package/src/components/ui/__tests__/preset-color-shadowing.test.ts +10 -10
- package/src/components/ui/__tests__/primary-subtle-token.test.ts +7 -7
- package/src/components/ui/__tests__/progress-secondary-contrast.test.ts +38 -38
- package/src/components/ui/__tests__/progress-warning-error-contrast.test.ts +64 -64
- package/src/components/ui/__tests__/quality-group-de.test.tsx +2 -2
- package/src/components/ui/__tests__/responsive-color-invariance.test.ts +20 -20
- package/src/components/ui/__tests__/select.test.tsx +6 -1
- package/src/components/ui/__tests__/semantic-token-usage.test.ts +23 -23
- package/src/components/ui/__tests__/size-scale-convention.test.ts +10 -10
- package/src/components/ui/__tests__/slider-accent-contrast.test.ts +142 -136
- package/src/components/ui/__tests__/slider-secondary-contrast.test.ts +60 -60
- package/src/components/ui/__tests__/slider-warning-error-contrast.test.ts +100 -100
- package/src/components/ui/__tests__/switch-variant-contrast.test.tsx +103 -103
- package/src/components/ui/__tests__/tooltip.test.tsx +122 -0
- package/src/components/ui/accordion.tsx +16 -3
- package/src/components/ui/alert.tsx +1 -1
- package/src/components/ui/badge.tsx +12 -1
- package/src/components/ui/button.tsx +95 -24
- package/src/components/ui/card.tsx +59 -5
- package/src/components/ui/dropdown-menu.tsx +99 -5
- package/src/components/ui/file-upload.tsx +4 -0
- package/src/components/ui/index.ts +4 -1
- package/src/components/ui/popover-pro.tsx +41 -0
- package/src/components/ui/popover.tsx +162 -7
- package/src/components/ui/rating.tsx +24 -2
- package/src/components/ui/select.tsx +4 -4
- package/src/components/ui/simple-editor.tsx +3 -18
- package/src/components/ui/tabs.tsx +16 -1
- package/src/components/ui/textarea.tsx +6 -1
- package/src/components/ui/toast.tsx +17 -2
- package/src/components/ui/tooltip.tsx +154 -2
- package/src/styles/tokens.css +34 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #649 — `Button.asChild` ölü ternary'den GERÇEK Radix `Slot` polimorfizmine.
|
|
3
|
+
*
|
|
4
|
+
* Kusur: `const Comp = asChild ? "button" : "button"` — ternary'nin iki dalı birebir
|
|
5
|
+
* aynıydı, yani prop tipte ve docs'ta vaat edilmesine rağmen HİÇBİR ŞEY yapmıyordu.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ Mevcut `button.test.tsx`'teki "renders as child component when asChild is true"
|
|
8
|
+
* testi bu kusuru YAKALAYAMIYORDU: kusurlu kodda `<a>` çocuğu native <button>'ın İÇİNE
|
|
9
|
+
* render ediliyordu, dolayısıyla `getByRole('link')` + `href` iddiaları YANLIŞ NEDENLE
|
|
10
|
+
* geçiyordu. Bu suite'in ayırt edici iddiası "<button> DOM'da BULUNMAMALI"dır — kusurlu
|
|
11
|
+
* kodda kırmızı, düzeltilmiş kodda yeşil.
|
|
12
|
+
*
|
|
13
|
+
* Ayrıca button'a özgü davranışların Slot dalındaki karşılıklarını kilitler:
|
|
14
|
+
* · `loading` spinner'ı ve `leftIcon`/`rightIcon` çocuğun İÇİNE taşınır (`Slottable`).
|
|
15
|
+
* · `disabled`/`loading` native `disabled` yerine `aria-disabled` + `data-disabled`
|
|
16
|
+
* ile ifade edilir — <a>/<span> native `disabled`'ı TANIMAZ.
|
|
17
|
+
* · Etkinleştirme (fare + klavye Enter, ikisi de click event'i üretir) engellenir.
|
|
18
|
+
* · Çocuk geçerli bir element değilse native <button>'a geri düşülür (Slot tek
|
|
19
|
+
* geçerli element ister; aksi hâlde SESSİZCE boş render ederdi).
|
|
20
|
+
*/
|
|
21
|
+
import * as React from 'react'
|
|
22
|
+
import { render, screen, fireEvent, createEvent } from '@testing-library/react'
|
|
23
|
+
import { Button } from '../button'
|
|
24
|
+
|
|
25
|
+
describe('#649 Button asChild — Radix Slot polimorfizmi', () => {
|
|
26
|
+
it('asChild ile <a> render eder, native <button> ÜRETMEZ', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Button asChild>
|
|
29
|
+
<a href="/test">Link Button</a>
|
|
30
|
+
</Button>
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const link = screen.getByRole('link', { name: /link button/i })
|
|
34
|
+
expect(link.tagName).toBe('A')
|
|
35
|
+
expect(link).toHaveAttribute('href', '/test')
|
|
36
|
+
// Ölü ternary'nin imzası: çocuk <a> bir <button>'ın içinde duruyordu.
|
|
37
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
38
|
+
expect(document.querySelector('button')).toBeNull()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('buttonVariants sınıfları çocuğa geçer', () => {
|
|
42
|
+
render(
|
|
43
|
+
<Button asChild variant="destructive" size="lg" rounded="full">
|
|
44
|
+
<a href="/x">Sil</a>
|
|
45
|
+
</Button>
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const link = screen.getByRole('link')
|
|
49
|
+
expect(link).toHaveClass('bg-destructive')
|
|
50
|
+
expect(link).toHaveClass('h-12')
|
|
51
|
+
expect(link).toHaveClass('rounded-full')
|
|
52
|
+
expect(link).toHaveClass('moonui-theme')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("çocuğun kendi className'i korunur ve birleştirilir", () => {
|
|
56
|
+
render(
|
|
57
|
+
<Button asChild className="custom-class">
|
|
58
|
+
<a href="/x" className="child-class">
|
|
59
|
+
Git
|
|
60
|
+
</a>
|
|
61
|
+
</Button>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const link = screen.getByRole('link')
|
|
65
|
+
expect(link).toHaveClass('custom-class')
|
|
66
|
+
expect(link).toHaveClass('child-class')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('loading spinner ve ikonlar çocuğun İÇİNE taşınır (Slottable)', () => {
|
|
70
|
+
render(
|
|
71
|
+
<Button
|
|
72
|
+
asChild
|
|
73
|
+
loading
|
|
74
|
+
leftIcon={<span data-testid="left-icon" />}
|
|
75
|
+
rightIcon={<span data-testid="right-icon" />}
|
|
76
|
+
>
|
|
77
|
+
<a href="/x">
|
|
78
|
+
<span data-testid="child-label">Yükleniyor</span>
|
|
79
|
+
</a>
|
|
80
|
+
</Button>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const link = screen.getByRole('link')
|
|
84
|
+
// Çocuğun kendi içeriği korunur…
|
|
85
|
+
expect(link).toContainElement(screen.getByTestId('child-label'))
|
|
86
|
+
// …spinner de onun içine girer (Slot'un `Children.only` tuzağına düşmeden).
|
|
87
|
+
expect(link.querySelector('svg')).not.toBeNull()
|
|
88
|
+
expect(link).toHaveAttribute('data-loading', '')
|
|
89
|
+
expect(link).toHaveAttribute('aria-busy', 'true')
|
|
90
|
+
// loading, ikonları gizleme sözleşmesini asChild'da da korur.
|
|
91
|
+
expect(screen.queryByTestId('left-icon')).not.toBeInTheDocument()
|
|
92
|
+
expect(screen.queryByTestId('right-icon')).not.toBeInTheDocument()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('loading yokken leftIcon/rightIcon çocuğun içinde render edilir', () => {
|
|
96
|
+
render(
|
|
97
|
+
<Button
|
|
98
|
+
asChild
|
|
99
|
+
leftIcon={<span data-testid="left-icon" />}
|
|
100
|
+
rightIcon={<span data-testid="right-icon" />}
|
|
101
|
+
>
|
|
102
|
+
<a href="/x">Git</a>
|
|
103
|
+
</Button>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const link = screen.getByRole('link')
|
|
107
|
+
expect(link).toContainElement(screen.getByTestId('left-icon'))
|
|
108
|
+
expect(link).toContainElement(screen.getByTestId('right-icon'))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('disabled: native `disabled` yerine aria-disabled + inert sınıflar verilir', () => {
|
|
112
|
+
render(
|
|
113
|
+
<Button asChild disabled>
|
|
114
|
+
<a href="/x">Git</a>
|
|
115
|
+
</Button>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const link = screen.getByRole('link')
|
|
119
|
+
// <a> native `disabled`ı tanımaz — DOM'a basılmamalı.
|
|
120
|
+
expect(link).not.toHaveAttribute('disabled')
|
|
121
|
+
expect(link).toHaveAttribute('aria-disabled', 'true')
|
|
122
|
+
expect(link).toHaveAttribute('data-disabled', '')
|
|
123
|
+
expect(link).toHaveClass('pointer-events-none')
|
|
124
|
+
expect(link).toHaveClass('opacity-50')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('loading da asChild dalında aria-disabled üretir', () => {
|
|
128
|
+
render(
|
|
129
|
+
<Button asChild loading>
|
|
130
|
+
<a href="/x">Git</a>
|
|
131
|
+
</Button>
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const link = screen.getByRole('link')
|
|
135
|
+
expect(link).toHaveAttribute('aria-disabled', 'true')
|
|
136
|
+
expect(link).not.toHaveAttribute('disabled')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('devre dışıyken tıklama handler çağrılmaz ve gezinme iptal edilir', () => {
|
|
140
|
+
const handleClick = jest.fn()
|
|
141
|
+
render(
|
|
142
|
+
<Button asChild disabled onClick={handleClick}>
|
|
143
|
+
<a href="/x">Git</a>
|
|
144
|
+
</Button>
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const link = screen.getByRole('link')
|
|
148
|
+
const clickEvent = createEvent.click(link)
|
|
149
|
+
fireEvent(link, clickEvent)
|
|
150
|
+
|
|
151
|
+
expect(handleClick).not.toHaveBeenCalled()
|
|
152
|
+
// <a> için preventDefault gezinmeyi durdurur (klavye Enter de click üretir).
|
|
153
|
+
expect(clickEvent.defaultPrevented).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('devre dışı değilken onClick normal çalışır', () => {
|
|
157
|
+
const handleClick = jest.fn()
|
|
158
|
+
render(
|
|
159
|
+
<Button asChild onClick={handleClick}>
|
|
160
|
+
<a href="/x">Git</a>
|
|
161
|
+
</Button>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
fireEvent.click(screen.getByRole('link'))
|
|
165
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('ref slot edilen elemana çözülür', () => {
|
|
169
|
+
const ref = React.createRef<HTMLElement>()
|
|
170
|
+
render(
|
|
171
|
+
<Button asChild ref={ref as React.Ref<HTMLButtonElement>}>
|
|
172
|
+
<a href="/x">Git</a>
|
|
173
|
+
</Button>
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
expect(ref.current).not.toBeNull()
|
|
177
|
+
expect(ref.current?.tagName).toBe('A')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('çocuk geçerli element değilse native <button>a geri düşer (boş render YOK)', () => {
|
|
181
|
+
// Geriye dönük güvenlik: `asChild` bugüne dek etkisizdi, düz metinle yazan
|
|
182
|
+
// mevcut kullanım Slot'a düşseydi HİÇBİR ŞEY render etmezdi.
|
|
183
|
+
render(<Button asChild>Düz metin</Button>)
|
|
184
|
+
|
|
185
|
+
const button = screen.getByRole('button', { name: /düz metin/i })
|
|
186
|
+
expect(button.tagName).toBe('BUTTON')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('asChild olmadan davranış DEĞİŞMEZ (native button + native disabled)', () => {
|
|
190
|
+
render(<Button disabled>Devre dışı</Button>)
|
|
191
|
+
|
|
192
|
+
const button = screen.getByRole('button')
|
|
193
|
+
expect(button.tagName).toBe('BUTTON')
|
|
194
|
+
expect(button).toBeDisabled()
|
|
195
|
+
// Slot dalına özgü öznitelikler normal dala SIZMAMALI.
|
|
196
|
+
expect(button).not.toHaveAttribute('aria-disabled')
|
|
197
|
+
expect(button).not.toHaveAttribute('data-disabled')
|
|
198
|
+
})
|
|
199
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue #664 — S4: `separator` dekoratif rolu (free ikiz).
|
|
3
|
+
*
|
|
4
|
+
* OLCULDU, KAYNAK DEGISTIRILMEDI. S4 tablosu `separator` icin
|
|
5
|
+
* "`decorative` → `role=\"none\"`" istiyordu; olcum bunun ZATEN saglandigini
|
|
6
|
+
* gosterdi:
|
|
7
|
+
*
|
|
8
|
+
* - `separator.tsx` `decorative` varsayilanini `true` verip Radix'e geciriyor,
|
|
9
|
+
* - `@radix-ui/react-separator@1.1.7` (`dist/index.js`) tam olarak sunu yapiyor:
|
|
10
|
+
* `decorative ? { role: "none" } : { "aria-orientation": ariaOrientation }`
|
|
11
|
+
*
|
|
12
|
+
* Yani gerekli olan tek sey durumun KILITLENMESI: Radix surum yukseltmesi,
|
|
13
|
+
* `decorative` varsayilaninin degismesi ya da prop'un Radix'e gecirilmesinin
|
|
14
|
+
* unutulmasi bu testi kirmizi yapar.
|
|
15
|
+
*
|
|
16
|
+
* Pro ikizi ayni iddiayla kilitli:
|
|
17
|
+
* packages/moonui-pro/src/__tests__/issue-664-s4-dekoratif-katmanlar.test.tsx
|
|
18
|
+
*/
|
|
19
|
+
import React from 'react'
|
|
20
|
+
import { render } from '@testing-library/react'
|
|
21
|
+
import '@testing-library/jest-dom'
|
|
22
|
+
import { Separator } from '../separator'
|
|
23
|
+
|
|
24
|
+
describe('#664 S4 — Separator dekoratif rolu', () => {
|
|
25
|
+
it('varsayilan (dekoratif) → erisilebilirlik agacindan cikaran role="none"', () => {
|
|
26
|
+
const { container } = render(<Separator />)
|
|
27
|
+
expect(container.firstElementChild).toHaveAttribute('role', 'none')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('`decorative={false}` → anlamli `role="separator"`', () => {
|
|
31
|
+
const { container } = render(<Separator decorative={false} />)
|
|
32
|
+
expect(container.firstElementChild).toHaveAttribute('role', 'separator')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('dekoratif rol gorsel varyantlari ETKILEMEZ (regresyon kapisi)', () => {
|
|
36
|
+
const { container } = render(<Separator orientation="vertical" variant="dashed" />)
|
|
37
|
+
const el = container.firstElementChild as HTMLElement
|
|
38
|
+
expect(el).toHaveAttribute('role', 'none')
|
|
39
|
+
expect(el.className).toContain('border-dashed')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -20,7 +20,7 @@ const extend = (preset as any).theme?.extend as Record<string, unknown> | undefi
|
|
|
20
20
|
const colors = extend?.colors as Record<string, ColorValue> | undefined
|
|
21
21
|
|
|
22
22
|
/** `border-*`, `text-*`, `bg-*` … ailelerini besleyen, renk gölgeleyebilen extend anahtarları */
|
|
23
|
-
const
|
|
23
|
+
const COLOR_FAMILIES = [
|
|
24
24
|
'borderColor',
|
|
25
25
|
'textColor',
|
|
26
26
|
'backgroundColor',
|
|
@@ -42,22 +42,22 @@ describe('tailwind preset — iç içe renk anahtarları gölgelenmiyor', () =>
|
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
it('hiçbir renk ailesi extend\'i, iç içe bir `colors` girdisini DÜZ STRING ile ezmiyor', () => {
|
|
45
|
-
const
|
|
46
|
-
for (const aile of
|
|
45
|
+
const violations: string[] = []
|
|
46
|
+
for (const aile of COLOR_FAMILIES) {
|
|
47
47
|
const blok = extend?.[aile] as Record<string, ColorValue> | undefined
|
|
48
48
|
if (!blok || typeof blok !== 'object') continue
|
|
49
|
-
for (const [isim,
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
const
|
|
53
|
-
|
|
49
|
+
for (const [isim, value] of Object.entries(blok)) {
|
|
50
|
+
const original = colors?.[isim]
|
|
51
|
+
if (original && typeof original === 'object' && typeof value === 'string') {
|
|
52
|
+
const missingKeys = Object.keys(original).filter((k) => k !== 'DEFAULT')
|
|
53
|
+
violations.push(
|
|
54
54
|
`theme.extend.${aile}.${isim} düz string → colors.${isim}'in ` +
|
|
55
|
-
`[${
|
|
55
|
+
`[${missingKeys.join(', ')}] alt anahtarları ${aile} için ÖLÜ`
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
expect(
|
|
60
|
+
expect(violations).toEqual([])
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('semantik renklerin subtle alt anahtarları preset\'te tanımlı', () => {
|
|
@@ -21,12 +21,12 @@ const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
|
|
|
21
21
|
const preset = fs.readFileSync(path.resolve(KOK, '../tailwind-preset.js'), 'utf8')
|
|
22
22
|
|
|
23
23
|
/** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
|
|
24
|
-
function tokenOku(
|
|
25
|
-
const
|
|
24
|
+
function tokenOku(name: string, blok: 'light' | 'dark'): string | null {
|
|
25
|
+
const source =
|
|
26
26
|
blok === 'dark'
|
|
27
27
|
? tokens.slice(tokens.indexOf('.dark'))
|
|
28
28
|
: tokens.slice(0, tokens.indexOf('.dark'))
|
|
29
|
-
return
|
|
29
|
+
return source.match(new RegExp(`--${name}:\\s*([^;]+);`))?.[1].trim() ?? null
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
@@ -41,7 +41,7 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
|
41
41
|
return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function
|
|
44
|
+
function relativeLuminance([r, g, b]: [number, number, number]): number {
|
|
45
45
|
const c = (v: number) => {
|
|
46
46
|
const x = v / 255
|
|
47
47
|
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
|
|
@@ -50,12 +50,12 @@ function bagilParlaklik([r, g, b]: [number, number, number]): number {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function kontrast(a: string, b: string): number {
|
|
53
|
-
const
|
|
53
|
+
const parse = (v: string): [number, number, number] => {
|
|
54
54
|
const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
|
|
55
55
|
return hslToRgb(h, s, l)
|
|
56
56
|
}
|
|
57
|
-
const la =
|
|
58
|
-
const lb =
|
|
57
|
+
const la = relativeLuminance(parse(a))
|
|
58
|
+
const lb = relativeLuminance(parse(b))
|
|
59
59
|
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
60
60
|
return (hi + 0.05) / (lo + 0.05)
|
|
61
61
|
}
|
|
@@ -35,12 +35,12 @@ const KOK = path.resolve(__dirname, '../../..')
|
|
|
35
35
|
const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
|
|
36
36
|
|
|
37
37
|
/** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
|
|
38
|
-
function tokenOku(
|
|
39
|
-
const
|
|
38
|
+
function tokenOku(name: string, blok: 'light' | 'dark'): string | null {
|
|
39
|
+
const source =
|
|
40
40
|
blok === 'dark'
|
|
41
41
|
? tokens.slice(tokens.indexOf('.dark'))
|
|
42
42
|
: tokens.slice(0, tokens.indexOf('.dark'))
|
|
43
|
-
return
|
|
43
|
+
return source.match(new RegExp(`--${name}:\\s*([^;]+);`))?.[1].trim() ?? null
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
@@ -70,7 +70,7 @@ const rgbTokenRgb = (v: string): [number, number, number] => {
|
|
|
70
70
|
return [p[0], p[1], p[2]]
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function
|
|
73
|
+
function relativeLuminance([r, g, b]: [number, number, number]): number {
|
|
74
74
|
const c = (v: number) => {
|
|
75
75
|
const x = v / 255
|
|
76
76
|
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
|
|
@@ -79,8 +79,8 @@ function bagilParlaklik([r, g, b]: [number, number, number]): number {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function rgbKontrast(a: [number, number, number], b: [number, number, number]): number {
|
|
82
|
-
const la =
|
|
83
|
-
const lb =
|
|
82
|
+
const la = relativeLuminance(a)
|
|
83
|
+
const lb = relativeLuminance(b)
|
|
84
84
|
const [hi, lo] = la > lb ? [la, lb] : [lb, la]
|
|
85
85
|
return (hi + 0.05) / (lo + 0.05)
|
|
86
86
|
}
|
|
@@ -89,12 +89,12 @@ function rgbKontrast(a: [number, number, number], b: [number, number, number]):
|
|
|
89
89
|
function alfaBileske(
|
|
90
90
|
ust: [number, number, number],
|
|
91
91
|
alfa: number,
|
|
92
|
-
|
|
92
|
+
backgroundVal: [number, number, number]
|
|
93
93
|
): [number, number, number] {
|
|
94
94
|
return [
|
|
95
|
-
Math.round(alfa * ust[0] + (1 - alfa) *
|
|
96
|
-
Math.round(alfa * ust[1] + (1 - alfa) *
|
|
97
|
-
Math.round(alfa * ust[2] + (1 - alfa) *
|
|
95
|
+
Math.round(alfa * ust[0] + (1 - alfa) * backgroundVal[0]),
|
|
96
|
+
Math.round(alfa * ust[1] + (1 - alfa) * backgroundVal[1]),
|
|
97
|
+
Math.round(alfa * ust[2] + (1 - alfa) * backgroundVal[2]),
|
|
98
98
|
]
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -103,20 +103,20 @@ function alfaBileske(
|
|
|
103
103
|
* #284/#368 konvansiyonu: kontrast, --background'ın yanında bu gerçek yüzeye
|
|
104
104
|
* göre de ölçülür (tokens.css --input notu).
|
|
105
105
|
*/
|
|
106
|
-
const
|
|
106
|
+
const CARD_ACTUAL_DARK: [number, number, number] = [17, 24, 39]
|
|
107
107
|
|
|
108
108
|
/** Tema başına dolgu (secondary-500) ve track (secondary@%20 ∘ zemin) renkleri */
|
|
109
|
-
function
|
|
110
|
-
const
|
|
109
|
+
function themeColors(blok: 'light' | 'dark') {
|
|
110
|
+
const fill = rgbTokenRgb(tokenOku('secondary-500', 'light')!) // skala yalnız :root'ta — iki temada aynı
|
|
111
111
|
const secondary = hslTokenRgb(tokenOku('secondary', blok)!)
|
|
112
112
|
const background = hslTokenRgb(tokenOku('background', blok)!)
|
|
113
113
|
const card = hslTokenRgb(tokenOku('card', blok)!)
|
|
114
|
-
const
|
|
114
|
+
const backgrounds: Array<[string, [number, number, number]]> = [
|
|
115
115
|
['--background', background],
|
|
116
116
|
['--card', card],
|
|
117
117
|
]
|
|
118
|
-
if (blok === 'dark')
|
|
119
|
-
return {
|
|
118
|
+
if (blok === 'dark') backgrounds.push(['gerçek kart #111827', CARD_ACTUAL_DARK])
|
|
119
|
+
return { fill, secondary, backgrounds }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
describe('#372 progress secondary dolgu↔track kontrastı', () => {
|
|
@@ -143,14 +143,14 @@ describe('#372 progress secondary dolgu↔track kontrastı', () => {
|
|
|
143
143
|
})
|
|
144
144
|
|
|
145
145
|
describe('kaynak sözleşmesi: dolgu ile track FARKLI token’lara basıyor', () => {
|
|
146
|
-
const
|
|
146
|
+
const source = fs.readFileSync(path.join(KOK, 'components/ui/progress.tsx'), 'utf8')
|
|
147
147
|
|
|
148
148
|
it('track secondary sınıfı `bg-secondary/20` (issue seçenek 1 — değişmedi)', () => {
|
|
149
|
-
expect(
|
|
149
|
+
expect(source).toMatch(/secondary:\s*"bg-secondary\/20"/)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
152
|
it('dolgu secondary sınıfı `bg-[rgb(var(--secondary-500))]` (artık --secondary DEĞİL)', () => {
|
|
153
|
-
expect(
|
|
153
|
+
expect(source).toMatch(/secondary:\s*"bg-\[rgb\(var\(--secondary-500\)\)\]"/)
|
|
154
154
|
})
|
|
155
155
|
})
|
|
156
156
|
|
|
@@ -159,17 +159,17 @@ describe('#372 progress secondary dolgu↔track kontrastı', () => {
|
|
|
159
159
|
render(React.createElement(Progress, { variant: 'secondary', value: 50 }))
|
|
160
160
|
const track = screen.getByRole('progressbar')
|
|
161
161
|
expect(track).toHaveClass('bg-secondary/20')
|
|
162
|
-
const
|
|
163
|
-
expect(
|
|
164
|
-
expect(
|
|
165
|
-
expect(
|
|
162
|
+
const fill = track.firstElementChild as HTMLElement
|
|
163
|
+
expect(fill).not.toBeNull()
|
|
164
|
+
expect(fill.className).toContain('bg-[rgb(var(--secondary-500))]')
|
|
165
|
+
expect(fill.className).not.toMatch(/(?:^|\s)bg-secondary(?:\s|$)/)
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
it('#318 düşüş mantığı KORUNUYOR — indicatorVariant hâlâ variant’ı ezer', () => {
|
|
169
169
|
render(React.createElement(Progress, { variant: 'secondary', indicatorVariant: 'success', value: 50 }))
|
|
170
|
-
const
|
|
171
|
-
expect(
|
|
172
|
-
expect(
|
|
170
|
+
const fill = screen.getByRole('progressbar').firstElementChild as HTMLElement
|
|
171
|
+
expect(fill.className).toContain('bg-success')
|
|
172
|
+
expect(fill.className).not.toContain('bg-[rgb(var(--secondary-500))]')
|
|
173
173
|
})
|
|
174
174
|
})
|
|
175
175
|
|
|
@@ -177,11 +177,11 @@ describe('#372 progress secondary dolgu↔track kontrastı', () => {
|
|
|
177
177
|
it('YENİ dolgu (secondary-500), track’in her olası zemindeki bileşkesine ≥3:1', () => {
|
|
178
178
|
// Ölçülen değerler: light 4.63 (--background=--card=beyaz) · dark 4.23
|
|
179
179
|
// (--background=--card) / 3.83 (gerçek kart #111827).
|
|
180
|
-
const {
|
|
181
|
-
for (const [
|
|
182
|
-
const track = alfaBileske(secondary, 0.2,
|
|
183
|
-
const oran = rgbKontrast(
|
|
184
|
-
expect({
|
|
180
|
+
const { fill, secondary, backgrounds } = themeColors(blok)
|
|
181
|
+
for (const [name, backgroundVal] of backgrounds) {
|
|
182
|
+
const track = alfaBileske(secondary, 0.2, backgroundVal)
|
|
183
|
+
const oran = rgbKontrast(fill, track)
|
|
184
|
+
expect({ backgroundVal: name, oran }).toEqual({ backgroundVal: name, oran: expect.any(Number) })
|
|
185
185
|
expect(oran).toBeGreaterThanOrEqual(3.0)
|
|
186
186
|
}
|
|
187
187
|
})
|
|
@@ -190,9 +190,9 @@ describe('#372 progress secondary dolgu↔track kontrastı', () => {
|
|
|
190
190
|
// Biri dolguyu `bg-secondary`ye geri döndürürse bu test ona "eski değer
|
|
191
191
|
// zaten görünmüyordu" diye değil, üstteki kriter testi kırmızıyla söyler;
|
|
192
192
|
// burası ise düzeltmenin GEREKLİLİĞİNİ sabitler (light 1.02, dark ~1.0-1.5).
|
|
193
|
-
const { secondary,
|
|
194
|
-
for (const [,
|
|
195
|
-
const track = alfaBileske(secondary, 0.2,
|
|
193
|
+
const { secondary, backgrounds } = themeColors(blok)
|
|
194
|
+
for (const [, backgroundVal] of backgrounds) {
|
|
195
|
+
const track = alfaBileske(secondary, 0.2, backgroundVal)
|
|
196
196
|
expect(rgbKontrast(secondary, track)).toBeLessThan(3.0)
|
|
197
197
|
}
|
|
198
198
|
})
|
|
@@ -204,15 +204,15 @@ describe('#372 progress secondary dolgu↔track kontrastı', () => {
|
|
|
204
204
|
// kademe dark track'e (near-black) yetişemiyor. Komşu kademeler bunu kanıtlar.
|
|
205
205
|
it('secondary-400 light temada eşiğin ALTINDA kalır (~2.50)', () => {
|
|
206
206
|
const k400 = rgbTokenRgb(tokenOku('secondary-400', 'light')!)
|
|
207
|
-
const { secondary,
|
|
208
|
-
const track = alfaBileske(secondary, 0.2,
|
|
207
|
+
const { secondary, backgrounds } = themeColors('light')
|
|
208
|
+
const track = alfaBileske(secondary, 0.2, backgrounds[0][1])
|
|
209
209
|
expect(rgbKontrast(k400, track)).toBeLessThan(3.0)
|
|
210
210
|
})
|
|
211
211
|
|
|
212
212
|
it('secondary-600 dark temada eşiğin ALTINDA kalır (~2.66)', () => {
|
|
213
213
|
const k600 = rgbTokenRgb(tokenOku('secondary-600', 'light')!)
|
|
214
|
-
const { secondary,
|
|
215
|
-
const track = alfaBileske(secondary, 0.2,
|
|
214
|
+
const { secondary, backgrounds } = themeColors('dark')
|
|
215
|
+
const track = alfaBileske(secondary, 0.2, backgrounds[0][1])
|
|
216
216
|
expect(rgbKontrast(k600, track)).toBeLessThan(3.0)
|
|
217
217
|
})
|
|
218
218
|
|