@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/dist/index.d.mts +17 -5
- package/dist/index.d.ts +17 -5
- package/dist/index.global.js +365 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +407 -334
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +371 -299
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/__tests__/alert.test.tsx +5 -5
- package/src/components/ui/__tests__/avatar.test.tsx +12 -12
- package/src/components/ui/__tests__/badge.test.tsx +17 -1
- package/src/components/ui/__tests__/select.test.tsx +24 -2
- package/src/components/ui/__tests__/switch.test.tsx +36 -3
- package/src/components/ui/alert.tsx +5 -2
- package/src/components/ui/avatar.tsx +18 -6
- package/src/components/ui/badge.tsx +18 -11
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/select.tsx +59 -23
- package/src/components/ui/switch.tsx +108 -51
package/package.json
CHANGED
|
@@ -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-
|
|
79
|
-
expect(alert).toHaveClass('text-
|
|
80
|
-
expect(alert).toHaveClass('border-
|
|
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', '
|
|
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', '
|
|
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
|
|
304
|
-
render(<AvatarGroup
|
|
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
|
|
313
|
-
render(<AvatarGroup
|
|
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
|
|
322
|
-
render(<AvatarGroup
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
345
|
-
render(<AvatarGroup
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
87
|
-
render(<Switch variant="
|
|
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="
|
|
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
|
|
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="
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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,
|
|
96
|
-
const
|
|
97
|
-
const
|
|
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-
|
|
26
|
-
"hover:bg-
|
|
27
|
-
"focus-visible:ring-
|
|
28
|
-
"dark:bg-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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-
|
|
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
|
|
85
|
-
<span className="text-muted-foreground
|
|
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 */
|