@moontra/moonui 3.4.0 → 4.1.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 (34) hide show
  1. package/dist/index.d.mts +34 -19
  2. package/dist/index.d.ts +34 -19
  3. package/dist/index.global.js +355 -42
  4. package/dist/index.global.js.map +1 -1
  5. package/dist/index.js +539 -387
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +505 -354
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/lib/theme.global.js +14 -1
  10. package/dist/lib/theme.global.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/components/ui/__tests__/alert.test.tsx +5 -5
  13. package/src/components/ui/__tests__/avatar.test.tsx +12 -12
  14. package/src/components/ui/__tests__/badge.test.tsx +17 -1
  15. package/src/components/ui/__tests__/label.test.tsx +3 -2
  16. package/src/components/ui/__tests__/select.test.tsx +24 -2
  17. package/src/components/ui/__tests__/switch.test.tsx +36 -3
  18. package/src/components/ui/__tests__/tags-input.test.tsx +163 -0
  19. package/src/components/ui/alert.tsx +5 -2
  20. package/src/components/ui/aspect-ratio.tsx +9 -3
  21. package/src/components/ui/avatar.tsx +18 -6
  22. package/src/components/ui/badge.tsx +18 -11
  23. package/src/components/ui/button.tsx +3 -0
  24. package/src/components/ui/date-picker.tsx +38 -19
  25. package/src/components/ui/file-upload.tsx +4 -0
  26. package/src/components/ui/index.ts +22 -0
  27. package/src/components/ui/input-otp.tsx +5 -0
  28. package/src/components/ui/input.tsx +7 -2
  29. package/src/components/ui/label.tsx +6 -3
  30. package/src/components/ui/select.tsx +59 -23
  31. package/src/components/ui/simple-editor.tsx +18 -3
  32. package/src/components/ui/switch.tsx +108 -51
  33. package/src/components/ui/tags-input.tsx +86 -14
  34. package/src/components/ui/textarea.tsx +8 -4
@@ -83,8 +83,8 @@ describe('Switch Component', () => {
83
83
  expect(switchElement).toHaveClass('data-[state=checked]:bg-warning')
84
84
  })
85
85
 
86
- it('renders danger variant correctly', () => {
87
- render(<Switch variant="danger" data-testid="switch" />)
86
+ it('renders destructive variant correctly', () => {
87
+ render(<Switch variant="destructive" data-testid="switch" />)
88
88
  const switchElement = screen.getByTestId('switch')
89
89
  expect(switchElement).toHaveClass('data-[state=checked]:bg-error')
90
90
  })
@@ -331,7 +331,7 @@ describe('Switch Component', () => {
331
331
  <Switch
332
332
  loading
333
333
  size="sm"
334
- variant="danger"
334
+ variant="destructive"
335
335
  leftIcon={<span>Left</span>}
336
336
  description="Loading switch"
337
337
  data-testid="switch"
@@ -369,4 +369,37 @@ describe('Switch Component', () => {
369
369
  expect(switchElement).toBeInTheDocument()
370
370
  })
371
371
  })
372
+
373
+ describe('description ↔ aria-describedby bağı (#271)', () => {
374
+ it('description verildiğinde switch ile programatik olarak ilişkilendirilir', () => {
375
+ // Regresyon: description düz <span> olarak render ediliyor ama Root'a
376
+ // hiç bağlanmıyordu — ekran okuyucu açıklamayı switch ile ilişkilendiremiyordu.
377
+ render(<Switch description="Bildirimleri aç" data-testid="switch" />)
378
+
379
+ const el = screen.getByTestId('switch')
380
+ const describedBy = el.getAttribute('aria-describedby')
381
+ expect(describedBy).toBeTruthy()
382
+
383
+ const descEl = document.getElementById(describedBy!)
384
+ expect(descEl).toHaveTextContent('Bildirimleri aç')
385
+ })
386
+
387
+ it('description yoksa aria-describedby basılmaz', () => {
388
+ render(<Switch data-testid="switch" />)
389
+ expect(screen.getByTestId('switch')).not.toHaveAttribute('aria-describedby')
390
+ })
391
+
392
+ it('tüketicinin kendi aria-describedby değeri korunur', () => {
393
+ render(
394
+ <>
395
+ <span id="dis">Harici açıklama</span>
396
+ <Switch description="Dahili" aria-describedby="dis" data-testid="switch" />
397
+ </>
398
+ )
399
+
400
+ const describedBy = screen.getByTestId('switch').getAttribute('aria-describedby')!
401
+ expect(describedBy.split(' ')).toContain('dis')
402
+ expect(describedBy.split(' ').length).toBe(2)
403
+ })
404
+ })
372
405
  })
@@ -0,0 +1,163 @@
1
+ /**
2
+ * issue #272 — TagsInput erişilebilirlik ve klavye gezintisi.
3
+ *
4
+ * Bileşenin hiç testi yoktu. Aşağıdakiler denetimde bulunan üç boşluğu
5
+ * kilitler: X butonunun erişilebilir adı, tag ekleme input'unun erişilebilir
6
+ * adı ve etiketler arası klavye gezintisi (önceden yalnızca Backspace ile SON
7
+ * etiket silinebiliyordu; aradaki bir etiketi klavyeyle silmenin yolu yoktu).
8
+ */
9
+ import * as React from 'react'
10
+ import { render, screen, fireEvent } from '@testing-library/react'
11
+ import '@testing-library/jest-dom'
12
+ import { TagsInput } from '../tags-input'
13
+
14
+ /** Kontrollü sarmalayıcı — bileşen controlled API'ye sahip. */
15
+ function Harness({
16
+ initial = ['alpha', 'beta', 'gamma'],
17
+ onChangeSpy,
18
+ ...rest
19
+ }: {
20
+ initial?: string[]
21
+ onChangeSpy?: jest.Mock
22
+ } & Partial<React.ComponentProps<typeof TagsInput>>) {
23
+ const [tags, setTags] = React.useState<string[]>(initial)
24
+ return (
25
+ <TagsInput
26
+ value={tags}
27
+ onChange={(next) => {
28
+ onChangeSpy?.(next)
29
+ setTags(next)
30
+ }}
31
+ {...rest}
32
+ />
33
+ )
34
+ }
35
+
36
+ const input = () => screen.getByRole('textbox')
37
+ const removeBtn = (tag: string) => screen.getByRole('button', { name: `Remove ${tag}` })
38
+
39
+ describe('TagsInput — erişilebilir adlar', () => {
40
+ it('tag ekleme input\'u erişilebilir ad taşır', () => {
41
+ render(<Harness />)
42
+ // Etiket eklendikçe placeholder kalkıyor; aria-label olmadan input adsız kalıyordu.
43
+ expect(input()).toHaveAccessibleName('Add tag')
44
+ })
45
+
46
+ it('tüketici kendi aria-label\'ını verebilir', () => {
47
+ render(<Harness aria-label="Etiketler" />)
48
+ expect(input()).toHaveAccessibleName('Etiketler')
49
+ })
50
+
51
+ it('her X butonu hangi etiketi sildiğini duyurur', () => {
52
+ render(<Harness />)
53
+ expect(removeBtn('alpha')).toBeInTheDocument()
54
+ expect(removeBtn('beta')).toBeInTheDocument()
55
+ expect(removeBtn('gamma')).toBeInTheDocument()
56
+ })
57
+
58
+ it('disabled iken silme butonları render edilmez', () => {
59
+ render(<Harness disabled />)
60
+ expect(screen.queryByRole('button', { name: /^Remove / })).not.toBeInTheDocument()
61
+ })
62
+ })
63
+
64
+ describe('TagsInput — etiket ekleme/silme', () => {
65
+ it('Enter ile etiket ekler', () => {
66
+ const spy = jest.fn()
67
+ render(<Harness initial={[]} onChangeSpy={spy} />)
68
+
69
+ fireEvent.change(input(), { target: { value: 'yeni' } })
70
+ fireEvent.keyDown(input(), { key: 'Enter' })
71
+
72
+ expect(spy).toHaveBeenCalledWith(['yeni'])
73
+ })
74
+
75
+ it('X butonuna tıklayınca doğru etiket silinir', () => {
76
+ const spy = jest.fn()
77
+ render(<Harness onChangeSpy={spy} />)
78
+
79
+ fireEvent.click(removeBtn('beta'))
80
+ expect(spy).toHaveBeenCalledWith(['alpha', 'gamma'])
81
+ })
82
+
83
+ it('boş input\'ta Backspace son etiketi siler', () => {
84
+ const spy = jest.fn()
85
+ render(<Harness onChangeSpy={spy} />)
86
+
87
+ fireEvent.keyDown(input(), { key: 'Backspace' })
88
+ expect(spy).toHaveBeenCalledWith(['alpha', 'beta'])
89
+ })
90
+
91
+ it('yinelenen etiket eklenmez ve hata duyurulur', () => {
92
+ render(<Harness />)
93
+
94
+ fireEvent.change(input(), { target: { value: 'alpha' } })
95
+ fireEvent.keyDown(input(), { key: 'Enter' })
96
+
97
+ const err = screen.getByRole('status')
98
+ expect(err).toHaveTextContent('This tag already exists')
99
+ // Hata input ile ilişkilendirilmeli.
100
+ expect(input()).toHaveAttribute('aria-invalid', 'true')
101
+ expect(input().getAttribute('aria-describedby')).toBe(err.id)
102
+ })
103
+ })
104
+
105
+ describe('TagsInput — klavye gezintisi (#272)', () => {
106
+ it('boş input\'ta sol ok son etikete odaklanır', () => {
107
+ render(<Harness />)
108
+
109
+ fireEvent.keyDown(input(), { key: 'ArrowLeft' })
110
+ expect(removeBtn('gamma')).toHaveFocus()
111
+ })
112
+
113
+ it('sol/sağ ok etiketler arasında gezinir', () => {
114
+ render(<Harness />)
115
+
116
+ removeBtn('gamma').focus()
117
+ fireEvent.keyDown(removeBtn('gamma'), { key: 'ArrowLeft' })
118
+ expect(removeBtn('beta')).toHaveFocus()
119
+
120
+ fireEvent.keyDown(removeBtn('beta'), { key: 'ArrowRight' })
121
+ expect(removeBtn('gamma')).toHaveFocus()
122
+ })
123
+
124
+ it('son etiketten sağ ok input\'a döner', () => {
125
+ render(<Harness />)
126
+
127
+ removeBtn('gamma').focus()
128
+ fireEvent.keyDown(removeBtn('gamma'), { key: 'ArrowRight' })
129
+ expect(input()).toHaveFocus()
130
+ })
131
+
132
+ it('Home/End uç etiketlere gider', () => {
133
+ render(<Harness />)
134
+
135
+ removeBtn('beta').focus()
136
+ fireEvent.keyDown(removeBtn('beta'), { key: 'Home' })
137
+ expect(removeBtn('alpha')).toHaveFocus()
138
+
139
+ fireEvent.keyDown(removeBtn('alpha'), { key: 'End' })
140
+ expect(removeBtn('gamma')).toHaveFocus()
141
+ })
142
+
143
+ it('odaklı etikette Delete o etiketi siler (hedefli silme)', () => {
144
+ const spy = jest.fn()
145
+ render(<Harness onChangeSpy={spy} />)
146
+
147
+ removeBtn('beta').focus()
148
+ fireEvent.keyDown(removeBtn('beta'), { key: 'Delete' })
149
+
150
+ // Backspace her zaman SONU siliyordu; ortadaki etiket klavyeyle silinemiyordu.
151
+ expect(spy).toHaveBeenCalledWith(['alpha', 'gamma'])
152
+ })
153
+
154
+ it('odaklı etikette Backspace de siler', () => {
155
+ const spy = jest.fn()
156
+ render(<Harness onChangeSpy={spy} />)
157
+
158
+ removeBtn('alpha').focus()
159
+ fireEvent.keyDown(removeBtn('alpha'), { key: 'Backspace' })
160
+
161
+ expect(spy).toHaveBeenCalledWith(['beta', 'gamma'])
162
+ })
163
+ })
@@ -23,7 +23,10 @@ const alertVariants = cva(
23
23
  success: "bg-success/10 text-success border-success/30",
24
24
  warning: "bg-warning/10 text-warning border-warning/30",
25
25
  error: "bg-destructive/10 text-destructive border-destructive/30",
26
- info: "bg-blue-500/10 text-blue-500 border-blue-500/30",
26
+ // `--info` token'ı (tokens.css) blue-500 ile birebir aynı değeri
27
+ // taşır → görsel değişiklik yok, ancak varyant artık temaya ve
28
+ // karanlık moda duyarlı. Kardeş varyantlarla simetrik.
29
+ info: "bg-info/10 text-info border-info/30",
27
30
  },
28
31
  size: {
29
32
  sm: "py-2 text-xs",
@@ -114,7 +117,7 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
114
117
  <button
115
118
  onClick={onClose}
116
119
  className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded-full opacity-70 transition-opacity hover:opacity-100"
117
- aria-label="Kapat"
120
+ aria-label="Close alert"
118
121
  >
119
122
  <X className="h-4 w-4" />
120
123
  </button>
@@ -17,11 +17,15 @@ const aspectRatioVariants = cva(
17
17
  "relative overflow-hidden",
18
18
  {
19
19
  variants: {
20
+ // `variant` yalnız yüzey/kenarlık eksenini yönetir. Önceden her varyant
21
+ // ayrıca `rounded-md` taşıyordu ve `radius` ekseniyle çakışıyordu
22
+ // (aynı anda iki `rounded-*` sınıfı üretiliyordu). Köşe yuvarlaması artık
23
+ // TEK kaynaktan: `radius`.
20
24
  variant: {
21
- default: "rounded-md bg-muted/10",
25
+ default: "bg-muted/10",
22
26
  ghost: "bg-transparent",
23
- outline: "rounded-md border border-border",
24
- card: "rounded-md bg-card shadow-sm",
27
+ outline: "border border-border",
28
+ card: "bg-card shadow-sm",
25
29
  },
26
30
  radius: {
27
31
  none: "rounded-none",
@@ -33,6 +37,8 @@ const aspectRatioVariants = cva(
33
37
  },
34
38
  defaultVariants: {
35
39
  variant: "default",
40
+ // Önceki davranış korunur: her varyant `rounded-md` taşıyordu.
41
+ radius: "md",
36
42
  },
37
43
  }
38
44
  )
@@ -85,16 +85,28 @@ const AvatarFallback = React.forwardRef<
85
85
  AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
86
86
 
87
87
  // Avatar Group Component for displaying multiple avatars
88
- interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
89
- limit?: number;
90
- avatars: React.ReactNode[];
88
+ //
89
+ // KIRICI (issue #260): prop sözleşmesi `limit`/`avatars: ReactNode[]` idi;
90
+ // pro paketteki kardeşi (`MoonUIAvatarGroupPro`) ise `max`/`children` kullanıyordu
91
+ // — aynı bileşen ailesi için iki farklı API. Standart React deseni (children)
92
+ // kanonik kabul edilip free tarafı pro'ya hizalandı.
93
+ //
94
+ // Geçiş: <AvatarGroup limit={3} avatars={[<Avatar/>, <Avatar/>]} />
95
+ // → <AvatarGroup max={3}><Avatar/><Avatar/></AvatarGroup>
96
+ export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
97
+ /** Gösterilecek en fazla avatar sayısı; kalanlar "+N" olarak özetlenir. */
98
+ max?: number;
99
+ /** Avatar bileşenleri. */
100
+ children?: React.ReactNode;
101
+ /** Avatar'lar arası üst üste binme miktarı (px, negatif). */
91
102
  overlapOffset?: number;
92
103
  }
93
104
 
94
105
  const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
95
- ({ className, limit, avatars, overlapOffset = -8, ...props }, ref) => {
96
- const visibleAvatars = limit ? avatars.slice(0, limit) : avatars;
97
- const remainingCount = limit ? Math.max(0, avatars.length - limit) : 0;
106
+ ({ className, max, children, overlapOffset = -8, ...props }, ref) => {
107
+ const childrenArray = React.Children.toArray(children);
108
+ const visibleAvatars = max ? childrenArray.slice(0, max) : childrenArray;
109
+ const remainingCount = max ? Math.max(0, childrenArray.length - max) : 0;
98
110
 
99
111
  return (
100
112
  <div
@@ -21,11 +21,15 @@ const badgeVariants = cva(
21
21
  {
22
22
  variants: {
23
23
  variant: {
24
+ // KIRICI (issue #261): önceden ham `gray-900`/`white` kullanılıyordu;
25
+ // aynı varyant pro pakette token'lıydı → free ve pro `primary` için
26
+ // farklı renk üretiyordu. Artık token'a bağlı (tema/karanlık moda duyarlı).
27
+ // `primary` VARSAYILAN varyant olduğu için görünür bir renk değişimidir.
24
28
  primary: [
25
- "border-transparent bg-gray-900 text-white",
26
- "hover:bg-gray-800 dark:hover:bg-gray-700",
27
- "focus-visible:ring-gray-500/30 dark:focus-visible:ring-gray-400/40",
28
- "dark:bg-gray-700 dark:text-gray-50 dark:shadow-inner dark:shadow-gray-950/10",
29
+ "border-transparent bg-primary text-primary-foreground",
30
+ "hover:bg-primary/90",
31
+ "focus-visible:ring-primary/30 dark:focus-visible:ring-primary/40",
32
+ "dark:bg-primary/90 dark:shadow-inner dark:shadow-primary/10",
29
33
  ],
30
34
  secondary: [
31
35
  "border-transparent bg-secondary text-secondary-foreground",
@@ -127,7 +131,7 @@ export interface BadgeProps
127
131
  * @param props.rightIcon - Badge'in sağında görüntülenecek ikon
128
132
  * @param props.noAutoSpacing - Otomatik spacing'i devre dışı bırak
129
133
  */
130
- function Badge({
134
+ const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(({
131
135
  className,
132
136
  variant,
133
137
  size,
@@ -141,7 +145,7 @@ function Badge({
141
145
  noAutoSpacing,
142
146
  children,
143
147
  ...props
144
- }: BadgeProps) {
148
+ }, ref) => {
145
149
  // Auto-assign icons and content for special variants
146
150
  let autoLeftIcon = leftIcon;
147
151
  let autoChildren = children;
@@ -169,14 +173,15 @@ function Badge({
169
173
  }
170
174
 
171
175
  return (
172
- <div
176
+ <div
177
+ ref={ref}
173
178
  className={cn(
174
- "moonui-theme",
175
- badgeVariants({ variant, size, radius }),
179
+ "moonui-theme",
180
+ badgeVariants({ variant, size, radius }),
176
181
  // Remove auto-spacing if noAutoSpacing is true
177
182
  noAutoSpacing && "mr-0 mb-0",
178
183
  className
179
- )}
184
+ )}
180
185
  data-removable={removable ? "" : undefined}
181
186
  {...props}
182
187
  >
@@ -234,6 +239,8 @@ function Badge({
234
239
  )}
235
240
  </div>
236
241
  );
237
- }
242
+ });
243
+
244
+ Badge.displayName = "Badge";
238
245
 
239
246
  export { Badge, badgeVariants };
@@ -205,6 +205,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
205
205
  className={cn("moonui-theme", buttonVariants({ variant, size, rounded, fullWidth, className }))}
206
206
  ref={ref}
207
207
  disabled={disabled || loading}
208
+ // Görsel spinner + data-loading vardı ama yardımcı teknolojiye meşgul
209
+ // durumu hiç duyurulmuyordu.
210
+ aria-busy={loading || undefined}
208
211
  data-loading={loading ? "" : undefined}
209
212
  {...props}
210
213
  >
@@ -121,7 +121,12 @@ export interface DatePickerProps
121
121
  calendarProps?: React.ComponentProps<typeof Calendar>;
122
122
  }
123
123
 
124
- export function DatePicker({
124
+ // NOT (issue #264): dört seçici de düz fonksiyondu, ref geçme yolu yoktu.
125
+ // Her biri KENDİ gerçek kök DOM elemanına bağlanır — DOM yapısı değişmez.
126
+ // DatePicker/DateTimePicker kök bir <div> render eder; DateRangePicker ve
127
+ // MonthPicker'ın kökü Radix `Popover`'dır ve kendisi DOM üretmez, bu yüzden
128
+ // onlarda ref tetikleyici butona gider.
129
+ export const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(function DatePicker({
125
130
  className,
126
131
  variant,
127
132
  size,
@@ -143,7 +148,7 @@ export function DatePicker({
143
148
  calendarProps,
144
149
  disabled,
145
150
  ...props
146
- }: DatePickerProps) {
151
+ }: DatePickerProps, ref: React.ForwardedRef<HTMLDivElement>) {
147
152
  const [open, setOpen] = React.useState(false);
148
153
  const [internalDate, setInternalDate] = React.useState<Date | undefined>(value);
149
154
 
@@ -163,7 +168,11 @@ export function DatePicker({
163
168
  }
164
169
  };
165
170
 
166
- const handleClear = (e: React.MouseEvent) => {
171
+ // Hem fare hem klavye (Enter/Space) yolundan çağrılıyor; yalnızca
172
+ // `stopPropagation` kullanıldığı için ortak taban tipi yeterli.
173
+ // (Önceden `React.MouseEvent` bekliyordu ve klavye yolunda `as any` ile
174
+ // zorlanıyordu.)
175
+ const handleClear = (e: React.SyntheticEvent) => {
167
176
  e.stopPropagation();
168
177
  handleSelect(undefined);
169
178
  };
@@ -188,7 +197,7 @@ export function DatePicker({
188
197
  const iconElement = icon || <CalendarIcon className="h-4 w-4" />;
189
198
 
190
199
  return (
191
- <div className="moonui-theme">
200
+ <div ref={ref} className="moonui-theme">
192
201
  <Popover open={open} onOpenChange={setOpen}>
193
202
  <PopoverTrigger asChild>
194
203
  <Button
@@ -231,7 +240,7 @@ export function DatePicker({
231
240
  onKeyDown={(e) => {
232
241
  if (e.key === 'Enter' || e.key === ' ') {
233
242
  e.preventDefault();
234
- handleClear(e as any);
243
+ handleClear(e);
235
244
  }
236
245
  }}
237
246
  >
@@ -255,7 +264,7 @@ export function DatePicker({
255
264
  </Button>
256
265
  </PopoverTrigger>
257
266
  <PopoverContent
258
- className="w-[360px] p-0 shadow-2xl rounded-2xl border border-gray-200 dark:border-gray-700 bg-background/95 backdrop-blur-sm !z-[9999]"
267
+ className="w-[360px] p-0 shadow-2xl rounded-2xl border border-border bg-background/95 backdrop-blur-sm !z-[9999]"
259
268
  align="start"
260
269
  sideOffset={12}
261
270
  >
@@ -289,7 +298,8 @@ export function DatePicker({
289
298
  </Popover>
290
299
  </div>
291
300
  );
292
- }
301
+ });
302
+ DatePicker.displayName = "DatePicker";
293
303
 
294
304
  /**
295
305
  * DateRangePicker Component
@@ -313,7 +323,7 @@ export interface DateRangePickerProps
313
323
  separator?: string;
314
324
  }
315
325
 
316
- export function DateRangePicker({
326
+ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(function DateRangePicker({
317
327
  className,
318
328
  value,
319
329
  onChange,
@@ -321,7 +331,7 @@ export function DateRangePicker({
321
331
  formatString = "LLL dd, y",
322
332
  separator = " - ",
323
333
  ...props
324
- }: DateRangePickerProps) {
334
+ }: DateRangePickerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
325
335
  const [open, setOpen] = React.useState(false);
326
336
  const [internalRange, setInternalRange] = React.useState(value);
327
337
 
@@ -352,6 +362,7 @@ export function DateRangePicker({
352
362
  <Popover open={open} onOpenChange={setOpen}>
353
363
  <PopoverTrigger asChild>
354
364
  <Button
365
+ ref={ref}
355
366
  variant={props.variant === "ghost" ? "ghost" : "outline"}
356
367
  className={cn(
357
368
  datePickerVariants({
@@ -370,7 +381,7 @@ export function DateRangePicker({
370
381
  </span>
371
382
  </Button>
372
383
  </PopoverTrigger>
373
- <PopoverContent className="w-auto p-0 shadow-2xl rounded-2xl border border-gray-200 dark:border-gray-700 bg-background/95 backdrop-blur-sm !z-[9999]" align="start" sideOffset={12}>
384
+ <PopoverContent className="w-auto p-0 shadow-2xl rounded-2xl border border-border bg-background/95 backdrop-blur-sm !z-[9999]" align="start" sideOffset={12}>
374
385
  <Calendar
375
386
  mode="range"
376
387
  defaultMonth={internalRange?.from}
@@ -383,7 +394,8 @@ export function DateRangePicker({
383
394
  </PopoverContent>
384
395
  </Popover>
385
396
  );
386
- }
397
+ });
398
+ DateRangePicker.displayName = "DateRangePicker";
387
399
 
388
400
  /**
389
401
  * DateTimePicker Component
@@ -406,7 +418,7 @@ export interface DateTimePickerProps extends DatePickerProps {
406
418
  timeInterval?: number;
407
419
  }
408
420
 
409
- export function DateTimePicker({
421
+ export const DateTimePicker = React.forwardRef<HTMLDivElement, DateTimePickerProps>(function DateTimePicker({
410
422
  value,
411
423
  onChange,
412
424
  formatString = "PPP p",
@@ -414,7 +426,7 @@ export function DateTimePicker({
414
426
  timeFormat = "24",
415
427
  timeInterval = 15,
416
428
  ...props
417
- }: DateTimePickerProps) {
429
+ }: DateTimePickerProps, ref: React.ForwardedRef<HTMLDivElement>) {
418
430
  const [date, setDate] = React.useState<Date | undefined>(value);
419
431
  const [time, setTime] = React.useState<string>(
420
432
  value ? format(value, timeFormat === "24" ? "HH:mm" : "hh:mm a") : "00:00"
@@ -469,7 +481,7 @@ export function DateTimePicker({
469
481
  }, [timeFormat, timeInterval]);
470
482
 
471
483
  return (
472
- <div className="flex flex-col gap-2">
484
+ <div ref={ref} className="flex flex-col gap-2">
473
485
  <DatePicker
474
486
  {...props}
475
487
  value={date}
@@ -500,7 +512,8 @@ export function DateTimePicker({
500
512
  )}
501
513
  </div>
502
514
  );
503
- }
515
+ });
516
+ DateTimePicker.displayName = "DateTimePicker";
504
517
 
505
518
  /**
506
519
  * MonthPicker Component
@@ -515,13 +528,13 @@ export interface MonthPickerProps extends Omit<DatePickerProps, "formatString">
515
528
  formatString?: string;
516
529
  }
517
530
 
518
- export function MonthPicker({
531
+ export const MonthPicker = React.forwardRef<HTMLButtonElement, MonthPickerProps>(function MonthPicker({
519
532
  value,
520
533
  onChange,
521
534
  placeholder = "Pick a month",
522
535
  formatString = "MMMM yyyy",
523
536
  ...props
524
- }: MonthPickerProps) {
537
+ }: MonthPickerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
525
538
  const [open, setOpen] = React.useState(false);
526
539
  const [viewDate, setViewDate] = React.useState(value || new Date());
527
540
 
@@ -550,6 +563,7 @@ export function MonthPicker({
550
563
  <Popover open={open} onOpenChange={setOpen}>
551
564
  <PopoverTrigger asChild>
552
565
  <Button
566
+ ref={ref}
553
567
  variant={props.variant === "default" ? "outline" : (props.variant || "outline")}
554
568
  className={cn(
555
569
  datePickerVariants({
@@ -566,12 +580,15 @@ export function MonthPicker({
566
580
  {displayValue || placeholder}
567
581
  </Button>
568
582
  </PopoverTrigger>
569
- <PopoverContent className="w-64 p-0 shadow-2xl border border-gray-200 dark:border-gray-700 bg-background !z-[9999]" align="start">
583
+ <PopoverContent className="w-64 p-0 shadow-2xl border border-border bg-background !z-[9999]" align="start">
570
584
  <div className="p-3">
571
585
  <div className="flex items-center justify-between mb-3">
586
+ {/* İkon-only butonlar: erişilebilir ad olmadan ekran okuyucu
587
+ yalnızca "button" der, hangi yöne gidileceği anlaşılmaz. */}
572
588
  <Button
573
589
  variant="outline"
574
590
  size="icon"
591
+ aria-label={`Previous year, ${viewDate.getFullYear() - 1}`}
575
592
  onClick={() => handleYearChange(-1)}
576
593
  >
577
594
  <ChevronLeft className="h-4 w-4" />
@@ -582,6 +599,7 @@ export function MonthPicker({
582
599
  <Button
583
600
  variant="outline"
584
601
  size="icon"
602
+ aria-label={`Next year, ${viewDate.getFullYear() + 1}`}
585
603
  onClick={() => handleYearChange(1)}
586
604
  >
587
605
  <ChevronRight className="h-4 w-4" />
@@ -604,7 +622,8 @@ export function MonthPicker({
604
622
  </PopoverContent>
605
623
  </Popover>
606
624
  );
607
- }
625
+ });
626
+ MonthPicker.displayName = "MonthPicker";
608
627
 
609
628
  // Re-export utilities
610
629
  export { format } from "date-fns";
@@ -325,6 +325,10 @@ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
325
325
  multiple={multiple}
326
326
  disabled={disabled}
327
327
  onChange={handleFileSelect}
328
+ // Input görsel olarak gizli (opacity-0) ve ilişkili bir <label>'ı
329
+ // yok — erişilebilir ad olmadan ekran okuyucu bunu adsız bir dosya
330
+ // alanı olarak duyuruyordu.
331
+ aria-label={multiple ? "Choose files to upload" : "Choose a file to upload"}
328
332
  className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
329
333
  />
330
334
 
@@ -34,6 +34,7 @@ export {
34
34
 
35
35
  export type {
36
36
  AvatarProps as MoonUIAvatarProps,
37
+ AvatarGroupProps as MoonUIAvatarGroupProps,
37
38
  } from "./avatar";
38
39
 
39
40
  // Badge
@@ -256,6 +257,10 @@ export type {
256
257
  // Label
257
258
  export { Label as MoonUILabel } from "./label";
258
259
 
260
+ export type {
261
+ LabelProps as MoonUILabelProps,
262
+ } from "./label";
263
+
259
264
  // LockedComponent
260
265
  export {
261
266
  LockedComponent as MoonUILockedComponent,
@@ -340,12 +345,21 @@ export {
340
345
  SelectScrollDownButton as MoonUISelectScrollDownButton,
341
346
  } from "./select";
342
347
 
348
+ export type {
349
+ SelectTriggerProps as MoonUISelectTriggerProps,
350
+ SelectItemProps as MoonUISelectItemProps,
351
+ } from "./select";
352
+
343
353
  // Separator
344
354
  export { Separator as MoonUISeparator } from "./separator";
345
355
 
346
356
  // SimpleEditor
347
357
  export { SimpleEditor as MoonUISimpleEditor } from "./simple-editor";
348
358
 
359
+ export type {
360
+ SimpleEditorProps as MoonUISimpleEditorProps,
361
+ } from "./simple-editor";
362
+
349
363
  // Skeleton
350
364
  export {
351
365
  Skeleton as MoonUISkeleton,
@@ -400,6 +414,14 @@ export {
400
414
  // TagsInput
401
415
  export { TagsInput as MoonUITagsInput } from "./tags-input";
402
416
 
417
+ export type {
418
+ TagsInputProps as MoonUITagsInputProps,
419
+ } from "./tags-input";
420
+
421
+ export type {
422
+ ToggleProps as MoonUIToggleProps,
423
+ } from "./toggle";
424
+
403
425
  // Textarea
404
426
  export { Textarea as MoonUITextarea } from "./textarea";
405
427
 
@@ -32,6 +32,11 @@ const InputOTP = React.forwardRef<
32
32
  >(({ className, containerClassName, ...props }, ref) => (
33
33
  <OTPInput
34
34
  ref={ref}
35
+ // input-otp gerçek girişi görsel olarak gizli bir <input> ile yapar ve o
36
+ // input'un ilişkili bir <label>'ı yok — erişilebilir ad olmadan ekran
37
+ // okuyucu adsız bir metin alanı duyuruyordu. Tüketici kendi aria-label'ını
38
+ // verirse aşağıdaki {...props} yayılımı bunu ezer.
39
+ aria-label="One-time password"
35
40
  containerClassName={cn(
36
41
  "moonui-theme",
37
42
  "flex items-center gap-2 has-[:disabled]:opacity-50",