@srcroot/ui 0.0.2 → 0.0.3
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 +1 -1
- package/registry/badge.tsx +9 -25
- package/registry/breadcrumb.tsx +1 -1
- package/registry/button-group.tsx +9 -29
- package/registry/button.tsx +20 -46
- package/registry/card.tsx +21 -47
- package/registry/combobox.tsx +0 -3
- package/registry/command.tsx +6 -4
- package/registry/container.tsx +9 -25
- package/registry/drawer.tsx +36 -12
- package/registry/dropdown-menu.tsx +92 -44
- package/registry/hover-card.tsx +1 -1
- package/registry/image.tsx +2 -2
- package/registry/menubar.tsx +1 -1
- package/registry/resizable.tsx +71 -33
- package/registry/scroll-area.tsx +66 -7
- package/registry/sheet.tsx +62 -18
- package/registry/sidebar.tsx +7 -0
- package/registry/slider.tsx +101 -86
- package/registry/text.tsx +7 -16
package/package.json
CHANGED
package/registry/badge.tsx
CHANGED
|
@@ -22,15 +22,12 @@ const badgeVariants = cva(
|
|
|
22
22
|
}
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
className?: string
|
|
29
|
-
children?: React.ReactNode
|
|
30
|
-
}
|
|
25
|
+
interface BadgeProps
|
|
26
|
+
extends React.HTMLAttributes<HTMLSpanElement>,
|
|
27
|
+
VariantProps<typeof badgeVariants> { }
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
|
-
*
|
|
30
|
+
* Badge component for status indicators
|
|
34
31
|
*
|
|
35
32
|
* @example
|
|
36
33
|
* // Default badge
|
|
@@ -38,26 +35,13 @@ interface BadgeBaseProps extends BadgeVariants {
|
|
|
38
35
|
*
|
|
39
36
|
* // Destructive variant
|
|
40
37
|
* <Badge variant="destructive">Error</Badge>
|
|
41
|
-
*
|
|
42
|
-
* // As a link
|
|
43
|
-
* <Badge as="a" href="/status">View Status</Badge>
|
|
44
38
|
*/
|
|
45
|
-
const Badge = React.forwardRef(
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
as,
|
|
49
|
-
className,
|
|
50
|
-
variant,
|
|
51
|
-
...props
|
|
52
|
-
}: BadgeBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof BadgeBaseProps | "as">,
|
|
53
|
-
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
54
|
-
) => {
|
|
55
|
-
const Comp = as || "span"
|
|
56
|
-
|
|
39
|
+
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
|
40
|
+
({ className, variant, ...props }, ref) => {
|
|
57
41
|
return (
|
|
58
|
-
<
|
|
59
|
-
ref={ref
|
|
60
|
-
className={cn(badgeVariants({ variant, className
|
|
42
|
+
<span
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
61
45
|
{...props}
|
|
62
46
|
/>
|
|
63
47
|
)
|
package/registry/breadcrumb.tsx
CHANGED
|
@@ -33,15 +33,12 @@ const buttonGroupVariants = cva("inline-flex", {
|
|
|
33
33
|
},
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
className?: string
|
|
40
|
-
children?: React.ReactNode
|
|
41
|
-
}
|
|
36
|
+
interface ButtonGroupProps
|
|
37
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
38
|
+
VariantProps<typeof buttonGroupVariants> { }
|
|
42
39
|
|
|
43
40
|
/**
|
|
44
|
-
*
|
|
41
|
+
* ButtonGroup to group buttons together
|
|
45
42
|
*
|
|
46
43
|
* @example
|
|
47
44
|
* <ButtonGroup>
|
|
@@ -49,31 +46,14 @@ interface ButtonGroupBaseProps extends ButtonGroupVariants {
|
|
|
49
46
|
* <Button>Center</Button>
|
|
50
47
|
* <Button>Right</Button>
|
|
51
48
|
* </ButtonGroup>
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* <ButtonGroup attached={false}>
|
|
55
|
-
* <Button>Spaced</Button>
|
|
56
|
-
* <Button>Buttons</Button>
|
|
57
|
-
* </ButtonGroup>
|
|
58
49
|
*/
|
|
59
|
-
const ButtonGroup = React.forwardRef(
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
as,
|
|
63
|
-
className,
|
|
64
|
-
orientation,
|
|
65
|
-
attached,
|
|
66
|
-
...props
|
|
67
|
-
}: ButtonGroupBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonGroupBaseProps | "as">,
|
|
68
|
-
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
69
|
-
) => {
|
|
70
|
-
const Comp = as || "div"
|
|
71
|
-
|
|
50
|
+
const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
51
|
+
({ className, orientation, attached, ...props }, ref) => {
|
|
72
52
|
return (
|
|
73
|
-
<
|
|
74
|
-
ref={ref
|
|
53
|
+
<div
|
|
54
|
+
ref={ref}
|
|
75
55
|
role="group"
|
|
76
|
-
className={cn(buttonGroupVariants({ orientation, attached, className
|
|
56
|
+
className={cn(buttonGroupVariants({ orientation, attached }), className)}
|
|
77
57
|
{...props}
|
|
78
58
|
/>
|
|
79
59
|
)
|
package/registry/button.tsx
CHANGED
|
@@ -32,68 +32,42 @@ const buttonVariants = cva(
|
|
|
32
32
|
}
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
children?: React.ReactNode
|
|
35
|
+
interface ButtonProps
|
|
36
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
+
VariantProps<typeof buttonVariants> {
|
|
38
|
+
asChild?: boolean
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
|
-
*
|
|
42
|
+
* Button component
|
|
44
43
|
*
|
|
45
44
|
* @example
|
|
46
|
-
* //
|
|
45
|
+
* // Default button
|
|
47
46
|
* <Button variant="outline">Click me</Button>
|
|
48
47
|
*
|
|
49
|
-
* // As a link
|
|
50
|
-
* <Button as="a" href="/home" variant="link">Go Home</Button>
|
|
51
|
-
*
|
|
52
48
|
* // With loading state
|
|
53
49
|
* <Button disabled>
|
|
54
50
|
* <LoadingSpinner /> Processing...
|
|
55
51
|
* </Button>
|
|
56
52
|
*/
|
|
57
|
-
const Button = React.forwardRef(
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
as
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}: ButtonBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonBaseProps | "as">,
|
|
66
|
-
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
67
|
-
) => {
|
|
68
|
-
const Comp = as || "button"
|
|
69
|
-
|
|
70
|
-
// Ensure proper keyboard handling for non-button elements
|
|
71
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
72
|
-
if (Comp !== "button" && (e.key === "Enter" || e.key === " ")) {
|
|
73
|
-
e.preventDefault()
|
|
74
|
-
; (e.currentTarget as HTMLElement).click()
|
|
75
|
-
}
|
|
76
|
-
// Call original onKeyDown if provided
|
|
77
|
-
const originalOnKeyDown = (props as any).onKeyDown
|
|
78
|
-
if (originalOnKeyDown) {
|
|
79
|
-
originalOnKeyDown(e)
|
|
80
|
-
}
|
|
53
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
54
|
+
({ className, variant, size, asChild = false, children, ...props }, ref) => {
|
|
55
|
+
if (asChild && React.isValidElement(children)) {
|
|
56
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
57
|
+
ref,
|
|
58
|
+
className: cn(buttonVariants({ variant, size }), className),
|
|
59
|
+
...props,
|
|
60
|
+
})
|
|
81
61
|
}
|
|
82
62
|
|
|
83
|
-
// Add role="button" for non-button elements
|
|
84
|
-
const accessibilityProps = Comp !== "button" ? {
|
|
85
|
-
role: "button",
|
|
86
|
-
tabIndex: 0,
|
|
87
|
-
onKeyDown: handleKeyDown,
|
|
88
|
-
} : {}
|
|
89
|
-
|
|
90
63
|
return (
|
|
91
|
-
<
|
|
92
|
-
ref={ref
|
|
93
|
-
className={cn(buttonVariants({ variant, size, className
|
|
94
|
-
{...accessibilityProps}
|
|
64
|
+
<button
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn(buttonVariants({ variant, size }), className)}
|
|
95
67
|
{...props}
|
|
96
|
-
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</button>
|
|
97
71
|
)
|
|
98
72
|
}
|
|
99
73
|
)
|
package/registry/card.tsx
CHANGED
|
@@ -15,33 +15,17 @@ import { cn } from "@/lib/utils"
|
|
|
15
15
|
* </Card>
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
className
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}: CardBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof CardBaseProps | "as">,
|
|
30
|
-
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
31
|
-
) => {
|
|
32
|
-
const Comp = as || "div"
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Comp
|
|
36
|
-
ref={ref as any}
|
|
37
|
-
className={cn(
|
|
38
|
-
"rounded-xl border bg-card text-card-foreground shadow",
|
|
39
|
-
className
|
|
40
|
-
)}
|
|
41
|
-
{...props}
|
|
42
|
-
/>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
18
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
19
|
+
({ className, ...props }, ref) => (
|
|
20
|
+
<div
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn(
|
|
23
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
45
29
|
)
|
|
46
30
|
Card.displayName = "Card"
|
|
47
31
|
|
|
@@ -57,26 +41,16 @@ const CardHeader = React.forwardRef<
|
|
|
57
41
|
))
|
|
58
42
|
CardHeader.displayName = "CardHeader"
|
|
59
43
|
|
|
60
|
-
const CardTitle = React.forwardRef
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<Comp
|
|
73
|
-
ref={ref as any}
|
|
74
|
-
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
75
|
-
{...props}
|
|
76
|
-
/>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
)
|
|
44
|
+
const CardTitle = React.forwardRef<
|
|
45
|
+
HTMLHeadingElement,
|
|
46
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<h3
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
))
|
|
80
54
|
CardTitle.displayName = "CardTitle"
|
|
81
55
|
|
|
82
56
|
const CardDescription = React.forwardRef<
|
package/registry/combobox.tsx
CHANGED
|
@@ -120,9 +120,6 @@ export function Combobox({
|
|
|
120
120
|
)
|
|
121
121
|
) : (
|
|
122
122
|
<div className="flex items-center gap-2">
|
|
123
|
-
{selectedOptions[0].icon && (
|
|
124
|
-
<selectedOptions[0].icon className="h-4 w-4 text-muted-foreground" />
|
|
125
|
-
) /* this effectively forces me to use a variable too */}
|
|
126
123
|
{(() => {
|
|
127
124
|
const Icon = selectedOptions[0].icon
|
|
128
125
|
return Icon ? <Icon className="h-4 w-4 text-muted-foreground" /> : null
|
package/registry/command.tsx
CHANGED
|
@@ -156,11 +156,13 @@ const CommandEmpty = React.forwardRef<HTMLDivElement, CommandEmptyProps>(
|
|
|
156
156
|
({ className, children, ...props }, ref) => {
|
|
157
157
|
const { search, items } = useCommandContext()
|
|
158
158
|
|
|
159
|
-
//
|
|
160
|
-
const
|
|
159
|
+
// Count how many items would be visible with the current search
|
|
160
|
+
const visibleCount = search
|
|
161
|
+
? items.filter(item => item.toLowerCase().includes(search.toLowerCase())).length
|
|
162
|
+
: items.length
|
|
161
163
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
// Only show empty message when user has typed something but no results match
|
|
165
|
+
if (!search || visibleCount > 0) {
|
|
164
166
|
return null
|
|
165
167
|
}
|
|
166
168
|
|
package/registry/container.tsx
CHANGED
|
@@ -18,38 +18,22 @@ const containerVariants = cva("mx-auto w-full px-4", {
|
|
|
18
18
|
},
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
className?: string
|
|
25
|
-
children?: React.ReactNode
|
|
26
|
-
}
|
|
21
|
+
interface ContainerProps
|
|
22
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
23
|
+
VariantProps<typeof containerVariants> { }
|
|
27
24
|
|
|
28
25
|
/**
|
|
29
|
-
*
|
|
26
|
+
* Container for max-width layouts
|
|
30
27
|
*
|
|
31
28
|
* @example
|
|
32
29
|
* <Container size="lg">Content</Container>
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* <Container as="section" size="md">Section content</Container>
|
|
36
30
|
*/
|
|
37
|
-
const Container = React.forwardRef(
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
as,
|
|
41
|
-
className,
|
|
42
|
-
size,
|
|
43
|
-
...props
|
|
44
|
-
}: ContainerBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ContainerBaseProps | "as">,
|
|
45
|
-
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
46
|
-
) => {
|
|
47
|
-
const Comp = as || "div"
|
|
48
|
-
|
|
31
|
+
const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
|
|
32
|
+
({ className, size, ...props }, ref) => {
|
|
49
33
|
return (
|
|
50
|
-
<
|
|
51
|
-
ref={ref
|
|
52
|
-
className={cn(containerVariants({ size, className
|
|
34
|
+
<div
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(containerVariants({ size }), className)}
|
|
53
37
|
{...props}
|
|
54
38
|
/>
|
|
55
39
|
)
|
package/registry/drawer.tsx
CHANGED
|
@@ -88,19 +88,21 @@ const DrawerClose = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttribut
|
|
|
88
88
|
)
|
|
89
89
|
DrawerClose.displayName = "DrawerClose"
|
|
90
90
|
|
|
91
|
-
// Drawer Overlay
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
// Drawer Overlay (internal)
|
|
92
|
+
interface DrawerOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
93
|
+
isAnimating: boolean
|
|
94
|
+
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
const DrawerOverlay = React.forwardRef<HTMLDivElement, DrawerOverlayProps>(
|
|
97
|
+
({ className, isAnimating, ...props }, ref) => {
|
|
98
|
+
const { onOpenChange } = useDrawer()
|
|
97
99
|
|
|
98
100
|
return (
|
|
99
101
|
<div
|
|
100
102
|
ref={ref}
|
|
101
103
|
className={cn(
|
|
102
|
-
"fixed inset-0 z-50 bg-black/80",
|
|
103
|
-
"
|
|
104
|
+
"fixed inset-0 z-50 bg-black/80 transition-opacity duration-300",
|
|
105
|
+
isAnimating ? "opacity-100" : "opacity-0",
|
|
104
106
|
className
|
|
105
107
|
)}
|
|
106
108
|
onClick={() => onOpenChange(false)}
|
|
@@ -119,6 +121,28 @@ interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
119
121
|
const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
|
|
120
122
|
({ className, children, side = "bottom", ...props }, ref) => {
|
|
121
123
|
const { open, onOpenChange } = useDrawer()
|
|
124
|
+
const [isVisible, setIsVisible] = React.useState(false)
|
|
125
|
+
const [isAnimating, setIsAnimating] = React.useState(false)
|
|
126
|
+
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
if (open) {
|
|
129
|
+
// First make visible (off-screen)
|
|
130
|
+
setIsVisible(true)
|
|
131
|
+
// Use a small timeout to ensure the browser has painted the initial state
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
setIsAnimating(true)
|
|
134
|
+
}, 10)
|
|
135
|
+
return () => clearTimeout(timer)
|
|
136
|
+
} else {
|
|
137
|
+
// Start close animation
|
|
138
|
+
setIsAnimating(false)
|
|
139
|
+
// Wait for animation to complete before hiding
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
setIsVisible(false)
|
|
142
|
+
}, 300)
|
|
143
|
+
return () => clearTimeout(timer)
|
|
144
|
+
}
|
|
145
|
+
}, [open])
|
|
122
146
|
|
|
123
147
|
// Close on Escape
|
|
124
148
|
React.useEffect(() => {
|
|
@@ -142,11 +166,11 @@ const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
|
|
|
142
166
|
}
|
|
143
167
|
}, [open])
|
|
144
168
|
|
|
145
|
-
if (!
|
|
169
|
+
if (!isVisible) return null
|
|
146
170
|
|
|
147
171
|
return (
|
|
148
172
|
<>
|
|
149
|
-
<DrawerOverlay />
|
|
173
|
+
<DrawerOverlay isAnimating={isAnimating} />
|
|
150
174
|
<div
|
|
151
175
|
ref={ref}
|
|
152
176
|
className={cn(
|
|
@@ -154,9 +178,9 @@ const DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(
|
|
|
154
178
|
"transition-transform duration-300 ease-out",
|
|
155
179
|
side === "bottom" && "inset-x-0 bottom-0 rounded-t-xl border-t",
|
|
156
180
|
side === "top" && "inset-x-0 top-0 rounded-b-xl border-b",
|
|
157
|
-
// Animation
|
|
158
|
-
side === "bottom" && "
|
|
159
|
-
side === "top" && "
|
|
181
|
+
// Animation states
|
|
182
|
+
side === "bottom" && (isAnimating ? "translate-y-0" : "translate-y-full"),
|
|
183
|
+
side === "top" && (isAnimating ? "translate-y-0" : "-translate-y-full"),
|
|
160
184
|
className
|
|
161
185
|
)}
|
|
162
186
|
{...props}
|
|
@@ -4,6 +4,7 @@ import { cn } from "@/lib/utils"
|
|
|
4
4
|
interface DropdownMenuContextValue {
|
|
5
5
|
open: boolean
|
|
6
6
|
onOpenChange: (open: boolean) => void
|
|
7
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null)
|
|
@@ -16,7 +17,7 @@ interface DropdownMenuProps {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* DropdownMenu component with keyboard navigation
|
|
20
|
+
* DropdownMenu component with keyboard navigation and proper positioning
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
22
23
|
* <DropdownMenu>
|
|
@@ -33,12 +34,13 @@ interface DropdownMenuProps {
|
|
|
33
34
|
*/
|
|
34
35
|
function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DropdownMenuProps) {
|
|
35
36
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
37
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
36
38
|
|
|
37
39
|
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
38
40
|
const setOpen = onOpenChange || setUncontrolledOpen
|
|
39
41
|
|
|
40
42
|
return (
|
|
41
|
-
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
43
|
+
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
|
|
42
44
|
<div className="relative inline-block text-left">
|
|
43
45
|
{children}
|
|
44
46
|
</div>
|
|
@@ -60,18 +62,25 @@ const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTrig
|
|
|
60
62
|
context.onOpenChange(!context.open)
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
// Combine refs
|
|
66
|
+
const combinedRef = (node: HTMLButtonElement | null) => {
|
|
67
|
+
(context.triggerRef as React.MutableRefObject<HTMLButtonElement | null>).current = node
|
|
68
|
+
if (typeof ref === 'function') ref(node)
|
|
69
|
+
else if (ref) ref.current = node
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
if (asChild && React.isValidElement(children)) {
|
|
64
73
|
return React.cloneElement(children as React.ReactElement<any>, {
|
|
65
74
|
onClick: handleClick,
|
|
66
75
|
"aria-expanded": context.open,
|
|
67
76
|
"aria-haspopup": "menu",
|
|
68
|
-
ref,
|
|
77
|
+
ref: combinedRef,
|
|
69
78
|
})
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
return (
|
|
73
82
|
<button
|
|
74
|
-
ref={
|
|
83
|
+
ref={combinedRef}
|
|
75
84
|
aria-expanded={context.open}
|
|
76
85
|
aria-haspopup="menu"
|
|
77
86
|
onClick={handleClick}
|
|
@@ -84,53 +93,94 @@ const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTrig
|
|
|
84
93
|
)
|
|
85
94
|
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
97
|
+
/** Alignment relative to trigger: 'start' | 'center' | 'end' */
|
|
98
|
+
align?: 'start' | 'center' | 'end'
|
|
99
|
+
/** Side of trigger to open: 'bottom' | 'top' */
|
|
100
|
+
side?: 'bottom' | 'top'
|
|
101
|
+
/** Offset from trigger in pixels */
|
|
102
|
+
sideOffset?: number
|
|
103
|
+
}
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
|
106
|
+
({ className, align = 'start', side = 'bottom', sideOffset = 4, ...props }, ref) => {
|
|
107
|
+
const context = React.useContext(DropdownMenuContext)
|
|
108
|
+
if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
|
|
109
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
113
|
+
if (context.open) {
|
|
114
|
+
const target = e.target as Node
|
|
115
|
+
const content = contentRef.current
|
|
116
|
+
const trigger = context.triggerRef.current
|
|
117
|
+
|
|
118
|
+
// Don't close if clicking inside content or trigger
|
|
119
|
+
if (content?.contains(target) || trigger?.contains(target)) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
context.onOpenChange(false)
|
|
123
|
+
}
|
|
98
124
|
}
|
|
99
|
-
}
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
127
|
+
if (e.key === "Escape" && context.open) {
|
|
128
|
+
context.onOpenChange(false)
|
|
129
|
+
}
|
|
104
130
|
}
|
|
105
|
-
}
|
|
106
131
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
document.addEventListener("click", handleClickOutside)
|
|
134
|
+
}, 0)
|
|
135
|
+
document.addEventListener("keydown", handleEscape)
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
clearTimeout(timer)
|
|
139
|
+
document.removeEventListener("click", handleClickOutside)
|
|
140
|
+
document.removeEventListener("keydown", handleEscape)
|
|
141
|
+
}
|
|
142
|
+
}, [context.open, context])
|
|
143
|
+
|
|
144
|
+
if (!context.open) return null
|
|
111
145
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
// Calculate alignment classes
|
|
147
|
+
const alignmentClasses = {
|
|
148
|
+
start: 'left-0',
|
|
149
|
+
center: 'left-1/2 -translate-x-1/2',
|
|
150
|
+
end: 'right-0',
|
|
116
151
|
}
|
|
117
|
-
}, [context.open, context])
|
|
118
152
|
|
|
119
|
-
|
|
153
|
+
// Calculate side classes
|
|
154
|
+
const sideClasses = {
|
|
155
|
+
bottom: `top-full mt-${sideOffset}`,
|
|
156
|
+
top: `bottom-full mb-${sideOffset}`,
|
|
157
|
+
}
|
|
120
158
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
ref={(node) => {
|
|
162
|
+
(contentRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
|
163
|
+
if (typeof ref === 'function') ref(node)
|
|
164
|
+
else if (ref) ref.current = node
|
|
165
|
+
}}
|
|
166
|
+
role="menu"
|
|
167
|
+
aria-orientation="vertical"
|
|
168
|
+
className={cn(
|
|
169
|
+
"absolute z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
170
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
171
|
+
alignmentClasses[align],
|
|
172
|
+
side === 'bottom' ? 'top-full' : 'bottom-full',
|
|
173
|
+
className
|
|
174
|
+
)}
|
|
175
|
+
style={{
|
|
176
|
+
marginTop: side === 'bottom' ? sideOffset : undefined,
|
|
177
|
+
marginBottom: side === 'top' ? sideOffset : undefined,
|
|
178
|
+
}}
|
|
179
|
+
{...props}
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
)
|
|
134
184
|
DropdownMenuContent.displayName = "DropdownMenuContent"
|
|
135
185
|
|
|
136
186
|
const DropdownMenuItem = React.forwardRef<
|
|
@@ -188,8 +238,6 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|
|
188
238
|
HTMLDivElement,
|
|
189
239
|
React.HTMLAttributes<HTMLDivElement> & { checked?: boolean; disabled?: boolean }
|
|
190
240
|
>(({ className, children, checked, disabled, onClick, ...props }, ref) => {
|
|
191
|
-
const context = React.useContext(DropdownMenuContext)
|
|
192
|
-
|
|
193
241
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
194
242
|
if (disabled) return
|
|
195
243
|
onClick?.(e)
|