@moontra/moonui 6.15.0 → 6.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moontra/moonui",
3
- "version": "6.15.0",
3
+ "version": "6.17.0",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -0,0 +1,95 @@
1
+ /**
2
+ * #354 — `--primary-subtle` token çifti ve kontrast değişmezi.
3
+ *
4
+ * BULGU: pro Sidebar'ın aktif öğesi `bg-primary/10 text-primary` kullanıyordu.
5
+ * Light temada bu bileşim **4.28:1** veriyor (WCAG AA metin için 4.5 ister) ve
6
+ * docs kabuğu bu Sidebar'ı kullandığı için ihlal HER sayfada basılıyordu.
7
+ * Dark tarafta `--primary` neredeyse beyaz olduğu için sorun yoktu (15.73:1).
8
+ *
9
+ * KÖK NEDEN: #313/#314/#323'te kurulan `--*-subtle` ailesi (yumuşak tint + koyu
10
+ * metin) `destructive/success/warning/info` için vardı ama **primary EKSİKTİ** —
11
+ * bu yüzden Sidebar ham `primary/10` kompozisyonuna düşmüştü.
12
+ *
13
+ * Bu test hem token'ların VARLIĞINI hem de KONTRAST oranlarını sabitler: değer
14
+ * değiştirilirse ve oran 4.5'in altına düşerse kırmızı olur.
15
+ */
16
+ import * as fs from 'fs'
17
+ import * as path from 'path'
18
+
19
+ const KOK = path.resolve(__dirname, '../../..')
20
+ const tokens = fs.readFileSync(path.join(KOK, 'styles/tokens.css'), 'utf8')
21
+ const preset = fs.readFileSync(path.resolve(KOK, '../tailwind-preset.js'), 'utf8')
22
+
23
+ /** "H S% L%" biçimindeki token değerini oku (light = :root, dark = .dark bloğu) */
24
+ function tokenOku(ad: string, blok: 'light' | 'dark'): string | null {
25
+ const kaynak =
26
+ blok === 'dark'
27
+ ? tokens.slice(tokens.indexOf('.dark'))
28
+ : tokens.slice(0, tokens.indexOf('.dark'))
29
+ return kaynak.match(new RegExp(`--${ad}:\\s*([^;]+);`))?.[1].trim() ?? null
30
+ }
31
+
32
+ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
33
+ h /= 360
34
+ s /= 100
35
+ l /= 100
36
+ const f = (n: number) => {
37
+ const k = (n + h * 12) % 12
38
+ const a = s * Math.min(l, 1 - l)
39
+ return l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
40
+ }
41
+ return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]
42
+ }
43
+
44
+ function bagilParlaklik([r, g, b]: [number, number, number]): number {
45
+ const c = (v: number) => {
46
+ const x = v / 255
47
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
48
+ }
49
+ return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b)
50
+ }
51
+
52
+ function kontrast(a: string, b: string): number {
53
+ const ayrıştır = (v: string): [number, number, number] => {
54
+ const [h, s, l] = v.split(/\s+/).map((x) => parseFloat(x))
55
+ return hslToRgb(h, s, l)
56
+ }
57
+ const la = bagilParlaklik(ayrıştır(a))
58
+ const lb = bagilParlaklik(ayrıştır(b))
59
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la]
60
+ return (hi + 0.05) / (lo + 0.05)
61
+ }
62
+
63
+ describe('#354 --primary-subtle token çifti', () => {
64
+ it('hesaplayıcı DOĞRU — bilinen bir çift beklenen oranı veriyor', () => {
65
+ // Sessiz-yeşil koruması: kontrast fonksiyonu bozulursa aşağıdaki testler
66
+ // anlamsız sayılarla yeşil geçebilir. `--info-subtle` çifti tokens.css'te
67
+ // 9.52:1 diye belgelenmiş; hesaplayıcı onu yeniden üretmeli.
68
+ const oran = kontrast(tokenOku('info-subtle', 'light')!, tokenOku('info-subtle-foreground', 'light')!)
69
+ expect(oran).toBeGreaterThan(9.3)
70
+ expect(oran).toBeLessThan(9.7)
71
+ })
72
+
73
+ it.each(['light', 'dark'] as const)('%s temada token çifti TANIMLI', (blok) => {
74
+ expect(tokenOku('primary-subtle', blok)).toBeTruthy()
75
+ expect(tokenOku('primary-subtle-foreground', blok)).toBeTruthy()
76
+ })
77
+
78
+ it.each(['light', 'dark'] as const)('%s temada kontrast WCAG AA (4.5:1) geçiyor', (blok) => {
79
+ const oran = kontrast(tokenOku('primary-subtle', blok)!, tokenOku('primary-subtle-foreground', blok)!)
80
+ expect(oran).toBeGreaterThanOrEqual(4.5)
81
+ })
82
+
83
+ it('light zemin, bugünkü `primary/10` görünümünü KORUYOR', () => {
84
+ // Zemin değişirse aktif öğenin görünümü değişir; bu bilinçli bir karar olmalı.
85
+ // #e7f0fe = beyaz üstüne %10 primary.
86
+ expect(tokenOku('primary-subtle', 'light')).toBe('217 92% 95%')
87
+ })
88
+
89
+ it('preset sınıfları ÜRETİYOR (token tanımlamak yetmez)', () => {
90
+ // Bu oturumda `theme.extend.borderColor`ın nested renkleri GÖLGELEDİĞİ ve
91
+ // sınıfın sessizce hiç CSS üretmediği görülmüştü (#313/#314/#323).
92
+ expect(preset).toContain('hsl(var(--primary-subtle) / <alpha-value>)')
93
+ expect(preset).toContain('hsl(var(--primary-subtle-foreground) / <alpha-value>)')
94
+ })
95
+ })
@@ -0,0 +1,65 @@
1
+ /**
2
+ * #340 — `SelectTrigger` erişilebilir ad taşır.
3
+ *
4
+ * BULGUNUN İNCELİĞİ: docs'ta hiçbir tetikleyici etiketsiz değildi — hepsinde
5
+ * `<SelectValue placeholder="Select a theme" />` vardı ve metin ekranda GÖRÜNÜYORDU.
6
+ * Yine de axe 10 `button-name` ihlali sayıyordu. Sebebi axe'ın açıklaması verdi:
7
+ *
8
+ * "Element does not have inner text that is visible to screen readers"
9
+ *
10
+ * `SelectPrimitive.Trigger` `role="combobox"` render eder ve ARIA'da bu rol
11
+ * **"name from author"**dur — erişilebilir ad İÇERİKTEN GELMEZ. Yani görünür metin
12
+ * gözle okunur ama ekran okuyucuya hiç ad taşımaz.
13
+ *
14
+ * Sonuç: `aria-label` verilmeyen HER MoonUI Select adsızdı. Bu bir docs örneği
15
+ * kusuru değil, component yüzeyiydi.
16
+ */
17
+ import * as React from 'react'
18
+ import { render, screen } from '@testing-library/react'
19
+ import '@testing-library/jest-dom'
20
+
21
+ import { Select, SelectTrigger, SelectValue } from '../select'
22
+
23
+ const kur = (props: React.ComponentProps<typeof SelectTrigger> = {}) =>
24
+ render(
25
+ <Select>
26
+ <SelectTrigger {...props}>
27
+ <SelectValue placeholder="Select a theme" />
28
+ </SelectTrigger>
29
+ </Select>
30
+ )
31
+
32
+ describe('#340 SelectTrigger erişilebilir ad', () => {
33
+ it('ad verilmezse GÖRÜNÜR içerik ada bağlanır', () => {
34
+ kur()
35
+ // `role="combobox"` içerikten ad almadığı için component `aria-labelledby`
36
+ // ile açıkça bağlamak zorunda.
37
+ expect(screen.getByRole('combobox')).toHaveAccessibleName('Select a theme')
38
+ })
39
+
40
+ it('tüketicinin `aria-label`i KAZANIR', () => {
41
+ kur({ 'aria-label': 'Theme selector' })
42
+ expect(screen.getByRole('combobox')).toHaveAccessibleName('Theme selector')
43
+ })
44
+
45
+ it('tüketicinin `aria-labelledby`si KORUNUR (ezilmez)', () => {
46
+ render(
47
+ <>
48
+ <span id="dis-etiket">Appearance</span>
49
+ <Select>
50
+ <SelectTrigger aria-labelledby="dis-etiket">
51
+ <SelectValue placeholder="Select a theme" />
52
+ </SelectTrigger>
53
+ </Select>
54
+ </>
55
+ )
56
+ expect(screen.getByRole('combobox')).toHaveAccessibleName('Appearance')
57
+ })
58
+
59
+ it('rol gerçekten `combobox` — bu testin varsayımı ayakta', () => {
60
+ // Radix rolü değiştirirse (ör. `button`) ad-içerikten-gelir kuralı geçerli olur
61
+ // ve bu bekçinin gerekçesi düşer. O zaman bilinçli gözden geçirilmeli.
62
+ kur()
63
+ expect(screen.getByRole('combobox')).toBeInTheDocument()
64
+ })
65
+ })
@@ -100,7 +100,24 @@ export interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeo
100
100
  const SelectTrigger = React.forwardRef<
101
101
  React.ElementRef<typeof SelectPrimitive.Trigger>,
102
102
  SelectTriggerProps
103
- >(({ className, children, variant, size, error, success, loading, leftIcon, rightIcon, ...props }, ref) => (
103
+ >(({ className, children, variant, size, error, success, loading, leftIcon, rightIcon, ...props }, ref) => {
104
+ /**
105
+ * #340: `SelectPrimitive.Trigger` `role="combobox"` render eder. ARIA'da bu rol
106
+ * **"name from author"**dur — yani erişilebilir ad İÇERİKTEN GELMEZ. Tetikleyicinin
107
+ * içindeki görünür metin ("Select a theme") gözle okunur ama ekran okuyucuya HİÇ
108
+ * ad taşımaz; axe bunu `button-name` (critical) sayar.
109
+ *
110
+ * Sonuç: `aria-label` verilmeyen HER MoonUI Select adsızdı — docs örneği değil,
111
+ * component yüzeyi. Bu yüzden tetikleyicinin görünür içeriğine bir id verilip
112
+ * `aria-labelledby` ile ona bağlanıyor: ad, kullanıcının GÖRDÜĞÜ metin olur.
113
+ *
114
+ * Tüketici kendi `aria-label`/`aria-labelledby`sini verdiyse ona dokunulmaz.
115
+ */
116
+ const icerikId = React.useId()
117
+ const adVerilmis =
118
+ props["aria-label"] != null || props["aria-labelledby"] != null
119
+
120
+ return (
104
121
  <SelectPrimitive.Trigger
105
122
  ref={ref}
106
123
  className={cn(
@@ -119,8 +136,9 @@ const SelectTrigger = React.forwardRef<
119
136
  data-error={error ? true : undefined}
120
137
  data-success={success ? true : undefined}
121
138
  data-loading={loading ? true : undefined}
139
+ aria-labelledby={adVerilmis ? props["aria-labelledby"] : icerikId}
122
140
  >
123
- <div className="flex items-center flex-1 gap-2">
141
+ <div id={icerikId} className="flex items-center flex-1 gap-2">
124
142
  {leftIcon && (
125
143
  <span className="flex items-center justify-center text-muted-foreground">
126
144
  {leftIcon}
@@ -143,7 +161,8 @@ const SelectTrigger = React.forwardRef<
143
161
  </SelectPrimitive.Icon>
144
162
  )}
145
163
  </SelectPrimitive.Trigger>
146
- ))
164
+ )
165
+ })
147
166
  SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
148
167
 
149
168
  /**
@@ -199,6 +199,17 @@
199
199
  --info-subtle: 214 100% 97%;
200
200
  --info-subtle-foreground: 224 64% 33%; /* 9.52:1 */
201
201
 
202
+ /* #354: `primary` ailesi bu listede EKSİKTİ. Pro Sidebar'ın aktif öğesi
203
+ "primary metin + primary/10 zemin" deseni kullanıyordu; light temada bu
204
+ 4.28:1 veriyor (gereken 4.5) — yani docs kabuğu her sayfada bir WCAG AA
205
+ ihlali basıyordu. Dark tarafta `--primary` neredeyse beyaz olduğu için
206
+ sorun yoktu (15.73:1, ölçüldü).
207
+
208
+ Zemin, bugünkü `primary/10` bileşiminin BİREBİR HSL karşılığı (beyaz üstüne
209
+ %10 primary = #e7f0fe) → görünüm DEĞİŞMEZ, yalnız metin koyulaşır. */
210
+ --primary-subtle: 217 92% 95%;
211
+ --primary-subtle-foreground: 217.2 91.2% 40%; /* 6.21:1 */
212
+
202
213
  --primary-50: 240 249 255;
203
214
 
204
215
  /* Tailwind preset'i rounded-lg/md/sm'yi var(--radius)'a bagliyor.
@@ -474,6 +485,12 @@
474
485
  --warning-subtle-foreground: 34 100% 92%; /* 14.43:1 */
475
486
  --info-subtle: 217 80% 13%;
476
487
  --info-subtle-foreground: 214 95% 93%; /* 14.11:1 */
488
+
489
+ /* #354: koyu temada mevcut görünüm KORUNUYOR — bugünkü bileşimin birebir
490
+ karşılığı (near-white/10 near-black üstüne). Dark zaten geçiyordu; buradaki
491
+ amaç token ailesini tamamlamak, dark'ın rengini değiştirmek değil. */
492
+ --primary-subtle: 225 18% 13%;
493
+ --primary-subtle-foreground: 210 40% 98%; /* 15.73:1 */
477
494
  --border: 216 34% 17%;
478
495
  /* #284: koyu temada form kontrolü kenarlığı 1.36 idi -> 40% ile ~3.1.
479
496
  Not: --input switch.tsx:27'de kapalı track ZEMİNİ olarak da kullanılıyor;
@@ -76,6 +76,13 @@ module.exports = {
76
76
  primary: {
77
77
  DEFAULT: "hsl(var(--primary) / <alpha-value>)",
78
78
  foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
79
+ // #354: `primary` yumuşak-ton yüzeyi — `bg-primary-subtle` +
80
+ // `text-primary-subtle-foreground`. Aile destructive/success/warning/
81
+ // info için vardı, primary EKSİKTİ ve pro Sidebar'ın aktif öğesi bu
82
+ // yüzden ham `primary/10` kompozisyonuna düşüp light temada 4.28:1
83
+ // veriyordu.
84
+ subtle: "hsl(var(--primary-subtle) / <alpha-value>)",
85
+ "subtle-foreground": "hsl(var(--primary-subtle-foreground) / <alpha-value>)",
79
86
  },
80
87
  secondary: {
81
88
  DEFAULT: "hsl(var(--secondary) / <alpha-value>)",