@moontra/moonui 3.4.0 → 4.0.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": "3.4.0",
3
+ "version": "4.0.0",
4
4
  "description": "Premium React component library with modern design and customization",
5
5
  "author": "MoonUI",
6
6
  "license": "MIT",
@@ -75,9 +75,9 @@ describe('Alert Components', () => {
75
75
  it('renders info variant correctly', () => {
76
76
  render(<Alert variant="info" data-testid="alert">Test</Alert>)
77
77
  const alert = screen.getByTestId('alert')
78
- expect(alert).toHaveClass('bg-blue-500/10')
79
- expect(alert).toHaveClass('text-blue-500')
80
- expect(alert).toHaveClass('border-blue-500/30')
78
+ expect(alert).toHaveClass('bg-info/10')
79
+ expect(alert).toHaveClass('text-info')
80
+ expect(alert).toHaveClass('border-info/30')
81
81
  })
82
82
 
83
83
  it('uses default variant as default', () => {
@@ -222,7 +222,7 @@ describe('Alert Components', () => {
222
222
  const alert = screen.getByTestId('alert')
223
223
  const closeButton = alert.querySelector('button')
224
224
  expect(closeButton).toBeInTheDocument()
225
- expect(closeButton).toHaveAttribute('aria-label', 'Kapat')
225
+ expect(closeButton).toHaveAttribute('aria-label', 'Close alert')
226
226
  })
227
227
 
228
228
  it('does not render close button when closable is true but onClose is not provided', () => {
@@ -281,7 +281,7 @@ describe('Alert Components', () => {
281
281
  render(<Alert closable onClose={handleClose} data-testid="alert">Test</Alert>)
282
282
  const alert = screen.getByTestId('alert')
283
283
  const closeButton = alert.querySelector('button')
284
- expect(closeButton).toHaveAttribute('aria-label', 'Kapat')
284
+ expect(closeButton).toHaveAttribute('aria-label', 'Close alert')
285
285
  })
286
286
  })
287
287
  })
@@ -300,8 +300,8 @@ describe('Avatar Component', () => {
300
300
  <Avatar key="5"><AvatarFallback>A5</AvatarFallback></Avatar>,
301
301
  ]
302
302
 
303
- it('renders all avatars when no limit is set', () => {
304
- render(<AvatarGroup avatars={mockAvatars} />)
303
+ it('renders all avatars when no max is set', () => {
304
+ render(<AvatarGroup>{mockAvatars}</AvatarGroup>)
305
305
  expect(screen.getByText('A1')).toBeInTheDocument()
306
306
  expect(screen.getByText('A2')).toBeInTheDocument()
307
307
  expect(screen.getByText('A3')).toBeInTheDocument()
@@ -309,8 +309,8 @@ describe('Avatar Component', () => {
309
309
  expect(screen.getByText('A5')).toBeInTheDocument()
310
310
  })
311
311
 
312
- it('limits avatars when limit is set', () => {
313
- render(<AvatarGroup avatars={mockAvatars} limit={3} />)
312
+ it('limits avatars when max is set', () => {
313
+ render(<AvatarGroup max={3}>{mockAvatars}</AvatarGroup>)
314
314
  expect(screen.getByText('A1')).toBeInTheDocument()
315
315
  expect(screen.getByText('A2')).toBeInTheDocument()
316
316
  expect(screen.getByText('A3')).toBeInTheDocument()
@@ -318,31 +318,31 @@ describe('Avatar Component', () => {
318
318
  expect(screen.queryByText('A5')).not.toBeInTheDocument()
319
319
  })
320
320
 
321
- it('shows remaining count when limit is exceeded', () => {
322
- render(<AvatarGroup avatars={mockAvatars} limit={3} />)
321
+ it('shows remaining count when max is exceeded', () => {
322
+ render(<AvatarGroup max={3}>{mockAvatars}</AvatarGroup>)
323
323
  expect(screen.getByText('+2')).toBeInTheDocument()
324
324
  })
325
325
 
326
326
  it('applies custom className', () => {
327
- render(<AvatarGroup avatars={mockAvatars} className="custom-group" />)
327
+ render(<AvatarGroup className="custom-group">{mockAvatars}</AvatarGroup>)
328
328
  const group = screen.getByText('A1').closest('.custom-group')
329
329
  expect(group).toBeInTheDocument()
330
330
  })
331
331
 
332
332
  it('forwards ref correctly', () => {
333
333
  const ref = React.createRef<HTMLDivElement>()
334
- render(<AvatarGroup ref={ref} avatars={mockAvatars} />)
334
+ render(<AvatarGroup ref={ref}>{mockAvatars}</AvatarGroup>)
335
335
  expect(ref.current).toBeInstanceOf(HTMLDivElement)
336
336
  })
337
337
 
338
338
  it('applies custom overlap offset', () => {
339
- render(<AvatarGroup avatars={mockAvatars} overlapOffset={-12} />)
339
+ render(<AvatarGroup overlapOffset={-12}>{mockAvatars}</AvatarGroup>)
340
340
  // This test verifies the component renders without error with custom offset
341
341
  expect(screen.getByText('A1')).toBeInTheDocument()
342
342
  })
343
343
 
344
- it('handles empty avatars array', () => {
345
- render(<AvatarGroup avatars={[]} data-testid="avatar-group" />)
344
+ it('handles empty children', () => {
345
+ render(<AvatarGroup data-testid="avatar-group" />)
346
346
  const group = screen.getByTestId('avatar-group')
347
347
  expect(group).toBeInTheDocument()
348
348
  expect(group).toHaveClass('flex')
@@ -351,7 +351,7 @@ describe('Avatar Component', () => {
351
351
 
352
352
  it('handles single avatar', () => {
353
353
  const singleAvatar = [<Avatar key="1"><AvatarFallback>SA</AvatarFallback></Avatar>]
354
- render(<AvatarGroup avatars={singleAvatar} />)
354
+ render(<AvatarGroup>{singleAvatar}</AvatarGroup>)
355
355
  expect(screen.getByText('SA')).toBeInTheDocument()
356
356
  expect(screen.queryByText('+')).not.toBeInTheDocument()
357
357
  })
@@ -357,8 +357,24 @@ describe('Badge Component', () => {
357
357
 
358
358
  const badge = screen.getByText('Focusable').parentElement!
359
359
  fireEvent.focus(badge)
360
-
360
+
361
361
  expect(onFocus).toHaveBeenCalledTimes(1)
362
362
  })
363
363
  })
364
+
365
+ describe('forwardRef (#261)', () => {
366
+ it('ref kök elemana iletilir', () => {
367
+ // Regresyon: Badge düz `function Badge(...)` idi — batch'teki diğer 7
368
+ // bileşen forwardRef kullanırken Badge ref alamıyordu.
369
+ const ref = React.createRef<HTMLDivElement>()
370
+ render(<Badge ref={ref}>Etiket</Badge>)
371
+
372
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
373
+ expect(ref.current).toHaveTextContent('Etiket')
374
+ })
375
+
376
+ it('displayName tanımlı', () => {
377
+ expect(Badge.displayName).toBe('Badge')
378
+ })
379
+ })
364
380
  })
@@ -138,8 +138,10 @@ describe('Select Components', () => {
138
138
  <SelectTrigger variant="standard">Content</SelectTrigger>
139
139
  )
140
140
 
141
+ // Varyantlar artık ham `gray-*` yerine semantik token'lara bağlı
142
+ // (issue #263/#268: tema ve karanlık mod hardcoded paleti etkileyemiyordu).
141
143
  let trigger = screen.getByTestId('select-trigger')
142
- expect(trigger).toHaveClass('border border-gray-300')
144
+ expect(trigger).toHaveClass('border border-input')
143
145
 
144
146
  rerender(<SelectTrigger variant="outline">Content</SelectTrigger>)
145
147
  trigger = screen.getByTestId('select-trigger')
@@ -147,7 +149,7 @@ describe('Select Components', () => {
147
149
 
148
150
  rerender(<SelectTrigger variant="ghost">Content</SelectTrigger>)
149
151
  trigger = screen.getByTestId('select-trigger')
150
- expect(trigger).toHaveClass('border-none bg-gray-100')
152
+ expect(trigger).toHaveClass('border-none bg-muted')
151
153
 
152
154
  rerender(<SelectTrigger variant="underline">Content</SelectTrigger>)
153
155
  trigger = screen.getByTestId('select-trigger')
@@ -702,4 +704,24 @@ describe('Select Components', () => {
702
704
  expect(trigger).toHaveAttribute('data-success', 'true')
703
705
  })
704
706
  })
707
+
708
+ describe('Erişilebilirlik — error durumu (#268)', () => {
709
+ it('error verildiğinde aria-invalid set edilir', () => {
710
+ // Regresyon: `error` yalnızca görsel `border-error` + `data-error` üretiyordu;
711
+ // yardımcı teknolojiye hiç duyurulmuyordu (Input/Textarea aynı kavramda
712
+ // `aria-invalid` set ediyor).
713
+ render(<SelectTrigger error>Content</SelectTrigger>)
714
+ expect(screen.getByTestId('select-trigger')).toHaveAttribute('aria-invalid', 'true')
715
+ })
716
+
717
+ it('error verilmediğinde aria-invalid basılmaz', () => {
718
+ render(<SelectTrigger>Content</SelectTrigger>)
719
+ expect(screen.getByTestId('select-trigger')).not.toHaveAttribute('aria-invalid')
720
+ })
721
+
722
+ it('error string olarak da verilebilir', () => {
723
+ render(<SelectTrigger error="Zorunlu alan">Content</SelectTrigger>)
724
+ expect(screen.getByTestId('select-trigger')).toHaveAttribute('aria-invalid', 'true')
725
+ })
726
+ })
705
727
  })
@@ -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
  })
@@ -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>
@@ -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 };
@@ -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
@@ -340,6 +341,11 @@ export {
340
341
  SelectScrollDownButton as MoonUISelectScrollDownButton,
341
342
  } from "./select";
342
343
 
344
+ export type {
345
+ SelectTriggerProps as MoonUISelectTriggerProps,
346
+ SelectItemProps as MoonUISelectItemProps,
347
+ } from "./select";
348
+
343
349
  // Separator
344
350
  export { Separator as MoonUISeparator } from "./separator";
345
351
 
@@ -3,6 +3,7 @@
3
3
  import * as React from "react"
4
4
  import * as SelectPrimitive from "@radix-ui/react-select"
5
5
  import { Check, ChevronDown, ChevronUp, Loader2 } from "lucide-react"
6
+ import { cva } from "class-variance-authority"
6
7
 
7
8
  import { cn } from "../../lib/utils"
8
9
 
@@ -24,7 +25,51 @@ const SelectValue = SelectPrimitive.Value
24
25
  type SelectTriggerVariant = "standard" | "outline" | "ghost" | "underline";
25
26
  type SelectTriggerSize = "sm" | "md" | "lg";
26
27
 
27
- interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
28
+ /**
29
+ * Trigger varyantları `cva()` ile yönetilir (kütüphane geneli desen). Önceden
30
+ * `variant === "x" && "..."` manuel zincirlemesi kullanılıyor ve tüm renkler ham
31
+ * `gray-*` paletiyle yazılıyordu — tema/karanlık mod bu değerleri etkileyemiyordu.
32
+ * Artık semantik token'lara bağlı (`border-input`, `bg-background`, `bg-muted`…).
33
+ */
34
+ const selectTriggerVariants = cva(
35
+ [
36
+ "flex w-full items-center justify-between gap-1 rounded-md transition-all duration-200",
37
+ "disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
38
+ "focus-visible:outline-none text-foreground",
39
+ ],
40
+ {
41
+ variants: {
42
+ variant: {
43
+ standard:
44
+ "border border-input bg-background hover:border-ring focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary",
45
+ outline:
46
+ "border border-input bg-transparent hover:border-ring focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary",
47
+ ghost:
48
+ "border-none bg-muted hover:bg-muted/80 focus-visible:ring-2 focus-visible:ring-primary/30",
49
+ underline:
50
+ "border-t-0 border-l-0 border-r-0 border-b border-input rounded-none px-0 bg-transparent hover:border-ring focus-visible:ring-0 focus-visible:border-b-2 focus-visible:border-primary",
51
+ },
52
+ size: {
53
+ sm: "h-8 text-xs px-2",
54
+ md: "h-10 text-sm px-3",
55
+ lg: "h-12 text-base px-4",
56
+ },
57
+ state: {
58
+ none: "",
59
+ error: "border-error focus-visible:ring-error/30 focus-visible:border-error",
60
+ success:
61
+ "border-success focus-visible:ring-success/30 focus-visible:border-success",
62
+ },
63
+ },
64
+ defaultVariants: {
65
+ variant: "standard",
66
+ size: "md",
67
+ state: "none",
68
+ },
69
+ }
70
+ )
71
+
72
+ export interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
28
73
  /** Visual variant */
29
74
  variant?: SelectTriggerVariant;
30
75
  /** Size */
@@ -44,45 +89,36 @@ interface SelectTriggerProps extends React.ComponentPropsWithoutRef<typeof Selec
44
89
  const SelectTrigger = React.forwardRef<
45
90
  React.ElementRef<typeof SelectPrimitive.Trigger>,
46
91
  SelectTriggerProps
47
- >(({ className, children, variant = "standard", size = "md", error, success, loading, leftIcon, rightIcon, ...props }, ref) => (
92
+ >(({ className, children, variant, size, error, success, loading, leftIcon, rightIcon, ...props }, ref) => (
48
93
  <SelectPrimitive.Trigger
49
94
  ref={ref}
50
95
  className={cn(
51
- /* Base styles */
52
- "flex w-full items-center justify-between gap-1 rounded-md transition-all duration-200",
53
- "disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
54
- "focus-visible:outline-none",
55
- /* Error state */
56
- error && "border-error focus-visible:ring-error/30 focus-visible:border-error",
57
- /* Success state */
58
- success && "border-success focus-visible:ring-success/30 focus-visible:border-success",
59
- /* Size variants */
60
- size === "sm" && "h-8 text-xs px-2",
61
- size === "md" && "h-10 text-sm px-3",
62
- size === "lg" && "h-12 text-base px-4",
63
- /* Visual variants */
64
- variant === "standard" && "border border-gray-300 dark:border-gray-700 bg-background dark:bg-gray-800/80 dark:shadow-inner dark:shadow-gray-950/10 dark:text-gray-200 hover:border-gray-400 dark:hover:border-gray-600 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20 focus-visible:border-primary dark:focus-visible:border-primary/80",
65
- variant === "outline" && "border border-gray-300 dark:border-gray-700 bg-transparent dark:text-gray-200 hover:border-gray-400 dark:hover:border-gray-600 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20 focus-visible:border-primary dark:focus-visible:border-primary/80",
66
- variant === "ghost" && "border-none bg-gray-100 dark:bg-gray-800 dark:shadow-inner dark:shadow-gray-950/10 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 focus-visible:ring-2 focus-visible:ring-primary/30 dark:focus-visible:ring-primary/20",
67
- variant === "underline" && "border-t-0 border-l-0 border-r-0 border-b border-gray-300 dark:border-gray-600 rounded-none px-0 dark:text-gray-200 dark:bg-transparent hover:border-gray-400 dark:hover:border-gray-500 focus-visible:ring-0 focus-visible:border-b-2 focus-visible:border-primary dark:focus-visible:border-primary/80",
96
+ selectTriggerVariants({
97
+ variant,
98
+ size,
99
+ state: error ? "error" : success ? "success" : "none",
100
+ }),
68
101
  className
69
102
  )}
70
103
  {...props}
71
104
  disabled={props.disabled || loading}
105
+ // `error` görsel olarak işaretleniyordu ama yardımcı teknolojiye hiç
106
+ // duyurulmuyordu; Input/Textarea aynı kavramda `aria-invalid` set ediyor.
107
+ aria-invalid={error ? true : undefined}
72
108
  data-error={error ? true : undefined}
73
109
  data-success={success ? true : undefined}
74
110
  data-loading={loading ? true : undefined}
75
111
  >
76
112
  <div className="flex items-center flex-1 gap-2">
77
113
  {leftIcon && (
78
- <span className="flex items-center justify-center text-gray-500 dark:text-gray-400">
114
+ <span className="flex items-center justify-center text-muted-foreground">
79
115
  {leftIcon}
80
116
  </span>
81
117
  )}
82
118
  {loading ? (
83
119
  <div className="flex items-center gap-2">
84
- <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground dark:text-gray-400" />
85
- <span className="text-muted-foreground dark:text-gray-400">Loading...</span>
120
+ <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
121
+ <span className="text-muted-foreground">Loading...</span>
86
122
  </div>
87
123
  ) : children}
88
124
  </div>
@@ -198,7 +234,7 @@ SelectLabel.displayName = SelectPrimitive.Label.displayName
198
234
  type SelectItemVariant = "default" | "subtle" | "destructive" | "success" | "warning";
199
235
  type SelectItemSize = "sm" | "md" | "lg";
200
236
 
201
- interface SelectItemProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
237
+ export interface SelectItemProps extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
202
238
  /** Visual variant */
203
239
  variant?: SelectItemVariant;
204
240
  /** Size */