@srcroot/ui 0.0.55 → 0.0.58
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/README.md +151 -151
- package/dist/index.d.ts +0 -0
- package/dist/index.js +120 -93
- package/package.json +7 -2
- package/src/registry/analytics/google-analytics.tsx +36 -39
- package/src/registry/analytics/google-tag-manager.tsx +62 -65
- package/src/registry/analytics/meta-pixel.tsx +44 -47
- package/src/registry/analytics/microsoft-clarity.tsx +31 -34
- package/src/registry/analytics/tiktok-pixel.tsx +34 -37
- package/src/registry/lib/utils.ts +0 -0
- package/src/registry/themes/v3/blue.css +157 -157
- package/src/registry/themes/v3/glass.css +153 -153
- package/src/registry/themes/v3/gray.css +157 -157
- package/src/registry/themes/v3/green.css +157 -157
- package/src/registry/themes/v3/neutral.css +157 -157
- package/src/registry/themes/v3/orange.css +157 -157
- package/src/registry/themes/v3/rose.css +157 -157
- package/src/registry/themes/v3/slate.css +157 -157
- package/src/registry/themes/v3/stone.css +157 -157
- package/src/registry/themes/v3/violet.css +186 -186
- package/src/registry/themes/v3/zinc.css +157 -157
- package/src/registry/themes/v4/blue.css +184 -184
- package/src/registry/themes/v4/glass.css +180 -180
- package/src/registry/themes/v4/gray.css +184 -184
- package/src/registry/themes/v4/green.css +184 -184
- package/src/registry/themes/v4/neutral.css +184 -184
- package/src/registry/themes/v4/orange.css +184 -184
- package/src/registry/themes/v4/rose.css +184 -184
- package/src/registry/themes/v4/slate.css +184 -184
- package/src/registry/themes/v4/stone.css +184 -184
- package/src/registry/themes/v4/violet.css +184 -184
- package/src/registry/themes/v4/zinc.css +184 -184
- package/src/registry/ui/accordion.tsx +164 -165
- package/src/registry/ui/alert-dialog.tsx +213 -214
- package/src/registry/ui/alert.tsx +73 -76
- package/src/registry/ui/aspect-ratio.tsx +44 -47
- package/src/registry/ui/avatar.tsx +96 -97
- package/src/registry/ui/badge.tsx +52 -55
- package/src/registry/ui/breadcrumb.tsx +147 -150
- package/src/registry/ui/button-group.tsx +64 -67
- package/src/registry/ui/button.tsx +71 -72
- package/src/registry/ui/calendar.tsx +514 -515
- package/src/registry/ui/card.tsx +88 -91
- package/src/registry/ui/carousel.tsx +214 -214
- package/src/registry/ui/chart.tsx +373 -373
- package/src/registry/ui/chatbot.tsx +86 -13
- package/src/registry/ui/checkbox.tsx +93 -94
- package/src/registry/ui/collapsible.tsx +107 -108
- package/src/registry/ui/combobox.tsx +171 -171
- package/src/registry/ui/command.tsx +300 -300
- package/src/registry/ui/container.tsx +44 -47
- package/src/registry/ui/context-menu.tsx +221 -221
- package/src/registry/ui/date-picker.tsx +228 -228
- package/src/registry/ui/dialog.tsx +269 -270
- package/src/registry/ui/drawer.tsx +10 -4
- package/src/registry/ui/dropdown-menu.tsx +529 -530
- package/src/registry/ui/empty-state.tsx +0 -2
- package/src/registry/ui/file-upload.tsx +0 -0
- package/src/registry/ui/floating-dock.tsx +0 -0
- package/src/registry/ui/form-field.tsx +91 -94
- package/src/registry/ui/google-analytics.tsx +38 -0
- package/src/registry/ui/google-tag-manager.tsx +64 -0
- package/src/registry/ui/hover-card.tsx +223 -223
- package/src/registry/ui/image.tsx +144 -147
- package/src/registry/ui/input-group.tsx +82 -85
- package/src/registry/ui/input.tsx +125 -125
- package/src/registry/ui/kbd.tsx +60 -63
- package/src/registry/ui/label.tsx +36 -37
- package/src/registry/ui/loading-spinner.tsx +108 -111
- package/src/registry/ui/map.tsx +0 -0
- package/src/registry/ui/marquee.tsx +2 -0
- package/src/registry/ui/menubar.tsx +246 -246
- package/src/registry/ui/meta-pixel.tsx +46 -0
- package/src/registry/ui/microsoft-clarity.tsx +33 -0
- package/src/registry/ui/native-select.tsx +49 -52
- package/src/registry/ui/otp-input.tsx +163 -155
- package/src/registry/ui/pagination.tsx +149 -152
- package/src/registry/ui/patterns.tsx +28 -0
- package/src/registry/ui/popover.tsx +226 -227
- package/src/registry/ui/progress.tsx +51 -52
- package/src/registry/ui/radio.tsx +99 -102
- package/src/registry/ui/resizable.tsx +314 -314
- package/src/registry/ui/scroll-animation.tsx +45 -0
- package/src/registry/ui/scroll-area.tsx +121 -122
- package/src/registry/ui/scroll-to-top.tsx +0 -0
- package/src/registry/ui/search.tsx +162 -150
- package/src/registry/ui/select.tsx +292 -293
- package/src/registry/ui/separator.tsx +46 -47
- package/src/registry/ui/sheet.tsx +6 -3
- package/src/registry/ui/sidebar.tsx +628 -628
- package/src/registry/ui/skeleton.tsx +26 -29
- package/src/registry/ui/slider.tsx +196 -197
- package/src/registry/ui/slot.tsx +69 -72
- package/src/registry/ui/star-rating.tsx +146 -134
- package/src/registry/ui/switch.tsx +72 -73
- package/src/registry/ui/table-of-contents.tsx +96 -96
- package/src/registry/ui/table.tsx +138 -139
- package/src/registry/ui/tabs.tsx +124 -125
- package/src/registry/ui/text.tsx +61 -64
- package/src/registry/ui/textarea.tsx +41 -42
- package/src/registry/ui/theme-switcher.tsx +66 -66
- package/src/registry/ui/tiktok-pixel.tsx +36 -0
- package/src/registry/ui/toast.tsx +97 -98
- package/src/registry/ui/toggle-group.tsx +129 -129
- package/src/registry/ui/toggle.tsx +72 -72
- package/src/registry/ui/tooltip.tsx +143 -144
- package/src/registry/ui/whatsapp.tsx +0 -0
|
@@ -1,530 +1,529 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { createPortal } from "react-dom"
|
|
5
|
-
import { cn } from "@/lib/utils"
|
|
6
|
-
import { Slot } from "@/components/ui/slot"
|
|
7
|
-
|
|
8
|
-
interface DropdownMenuContextValue {
|
|
9
|
-
open: boolean
|
|
10
|
-
onOpenChange: (open: boolean) => void
|
|
11
|
-
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null)
|
|
15
|
-
|
|
16
|
-
interface DropdownMenuProps {
|
|
17
|
-
children: React.ReactNode
|
|
18
|
-
open?: boolean
|
|
19
|
-
onOpenChange?: (open: boolean) => void
|
|
20
|
-
defaultOpen?: boolean
|
|
21
|
-
className?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* DropdownMenu component with keyboard navigation and proper positioning
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* <DropdownMenu>
|
|
29
|
-
* <DropdownMenuTrigger asChild>
|
|
30
|
-
* <Button>Open Menu</Button>
|
|
31
|
-
* </DropdownMenuTrigger>
|
|
32
|
-
* <DropdownMenuContent>
|
|
33
|
-
* <DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
34
|
-
* <DropdownMenuSeparator />
|
|
35
|
-
* <DropdownMenuItem>Profile</DropdownMenuItem>
|
|
36
|
-
* <DropdownMenuItem>Settings</DropdownMenuItem>
|
|
37
|
-
* </DropdownMenuContent>
|
|
38
|
-
* </DropdownMenu>
|
|
39
|
-
*/
|
|
40
|
-
function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpen = false, className }: DropdownMenuProps) {
|
|
41
|
-
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
42
|
-
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
43
|
-
|
|
44
|
-
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
45
|
-
const setOpen = onOpenChange || setUncontrolledOpen
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
|
|
49
|
-
<div className={cn("relative inline-block text-left", className)}>
|
|
50
|
-
{children}
|
|
51
|
-
</div>
|
|
52
|
-
</DropdownMenuContext.Provider>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
interface DropdownMenuTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
58
|
-
asChild?: boolean
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTriggerProps>(
|
|
62
|
-
({ onClick, asChild, children, ...props }, ref) => {
|
|
63
|
-
const context = React.useContext(DropdownMenuContext)
|
|
64
|
-
if (!context) throw new Error("DropdownMenuTrigger must be used within DropdownMenu")
|
|
65
|
-
|
|
66
|
-
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
67
|
-
onClick?.(e)
|
|
68
|
-
context.onOpenChange(!context.open)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Combine refs
|
|
72
|
-
const combinedRef = (node: HTMLButtonElement | null) => {
|
|
73
|
-
(context.triggerRef as any).current = node
|
|
74
|
-
if (typeof ref === 'function') ref(node)
|
|
75
|
-
else if (ref) ref.current = node
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (asChild) {
|
|
79
|
-
// For asChild, we need to manually make sure the ref is passed correctly.
|
|
80
|
-
// Slot handles merging the passed ref with the child's ref.
|
|
81
|
-
// But here we're passing `combinedRef` as the ref to Slot.
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const Comp = asChild ? Slot : "button"
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<Comp
|
|
88
|
-
ref={combinedRef}
|
|
89
|
-
aria-expanded={context.open}
|
|
90
|
-
aria-haspopup="menu"
|
|
91
|
-
onClick={handleClick}
|
|
92
|
-
{...props}
|
|
93
|
-
>
|
|
94
|
-
{children}
|
|
95
|
-
</Comp>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
)
|
|
99
|
-
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
|
|
100
|
-
|
|
101
|
-
interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
102
|
-
/** Alignment relative to trigger: 'start' | 'center' | 'end' */
|
|
103
|
-
align?: 'start' | 'center' | 'end'
|
|
104
|
-
/** Side of trigger to open: 'bottom' | 'top' */
|
|
105
|
-
side?: 'bottom' | 'top'
|
|
106
|
-
/** Offset from trigger in pixels */
|
|
107
|
-
sideOffset?: number
|
|
108
|
-
/** Whether to render in a portal (default: true) */
|
|
109
|
-
portal?: boolean
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
|
113
|
-
({ className, align = 'start', side = 'bottom', sideOffset = 4, portal = true, ...props }, ref) => {
|
|
114
|
-
const context = React.useContext(DropdownMenuContext)
|
|
115
|
-
if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
|
|
116
|
-
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
117
|
-
const [position, setPosition] = React.useState({ top: 0, left: 0 })
|
|
118
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
119
|
-
const [currentSide, setCurrentSide] = React.useState(side)
|
|
120
|
-
|
|
121
|
-
// Reset scroll on mount/unmount if needed, but mainly we just need a portal container
|
|
122
|
-
const [mounted, setMounted] = React.useState(false)
|
|
123
|
-
React.useEffect(() => {
|
|
124
|
-
setMounted(true)
|
|
125
|
-
}, [])
|
|
126
|
-
|
|
127
|
-
React.useEffect(() => {
|
|
128
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
129
|
-
if (context.open) {
|
|
130
|
-
const target = e.target as Node
|
|
131
|
-
const content = contentRef.current
|
|
132
|
-
const trigger = context.triggerRef.current
|
|
133
|
-
|
|
134
|
-
// Don't close if clicking inside content or trigger
|
|
135
|
-
if (content?.contains(target) || trigger?.contains(target)) {
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
context.onOpenChange(false)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
143
|
-
if (e.key === "Escape" && context.open) {
|
|
144
|
-
context.onOpenChange(false)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const checkPosition = () => {
|
|
149
|
-
if (context.open && contentRef.current && context.triggerRef.current) {
|
|
150
|
-
const triggerRect = context.triggerRef.current.getBoundingClientRect()
|
|
151
|
-
const contentRect = contentRef.current.getBoundingClientRect()
|
|
152
|
-
const viewportHeight = window.innerHeight
|
|
153
|
-
const viewportWidth = window.innerWidth
|
|
154
|
-
|
|
155
|
-
let top = 0
|
|
156
|
-
let left = 0
|
|
157
|
-
let usedSide = side
|
|
158
|
-
|
|
159
|
-
// Vertical positioning logic
|
|
160
|
-
const spaceBelow = viewportHeight - triggerRect.bottom
|
|
161
|
-
const spaceAbove = triggerRect.top
|
|
162
|
-
const neededHeight = contentRect.height + sideOffset
|
|
163
|
-
|
|
164
|
-
if (side === 'bottom') {
|
|
165
|
-
if (spaceBelow < neededHeight && spaceAbove > neededHeight) {
|
|
166
|
-
usedSide = 'top'
|
|
167
|
-
}
|
|
168
|
-
} else if (side === 'top') {
|
|
169
|
-
if (spaceAbove < neededHeight && spaceBelow > neededHeight) {
|
|
170
|
-
usedSide = 'bottom'
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
setCurrentSide(usedSide)
|
|
174
|
-
|
|
175
|
-
if (usedSide === 'bottom') {
|
|
176
|
-
top = triggerRect.bottom + sideOffset
|
|
177
|
-
} else {
|
|
178
|
-
top = triggerRect.top - contentRect.height - sideOffset
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Horizontal positioning logic (Alignment)
|
|
182
|
-
if (align === 'start') {
|
|
183
|
-
left = triggerRect.left
|
|
184
|
-
} else if (align === 'end') {
|
|
185
|
-
left = triggerRect.right - contentRect.width
|
|
186
|
-
} else {
|
|
187
|
-
// center
|
|
188
|
-
left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Collision detection / clamping for X axis
|
|
192
|
-
if (left < 4) left = 4
|
|
193
|
-
if (left + contentRect.width > viewportWidth - 4) {
|
|
194
|
-
left = viewportWidth - contentRect.width - 4
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
setPosition({ top, left })
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (context.open) {
|
|
202
|
-
// Check position immediately after render cycle
|
|
203
|
-
requestAnimationFrame(checkPosition)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const timer = setTimeout(() => {
|
|
207
|
-
document.addEventListener("click", handleClickOutside)
|
|
208
|
-
}, 0)
|
|
209
|
-
document.addEventListener("keydown", handleEscape)
|
|
210
|
-
window.addEventListener("resize", checkPosition)
|
|
211
|
-
window.addEventListener("scroll", checkPosition, true)
|
|
212
|
-
|
|
213
|
-
return () => {
|
|
214
|
-
clearTimeout(timer)
|
|
215
|
-
document.removeEventListener("click", handleClickOutside)
|
|
216
|
-
document.removeEventListener("keydown", handleEscape)
|
|
217
|
-
window.removeEventListener("resize", checkPosition)
|
|
218
|
-
window.removeEventListener("scroll", checkPosition, true)
|
|
219
|
-
}
|
|
220
|
-
}, [context.open, context, side, sideOffset, align])
|
|
221
|
-
|
|
222
|
-
if (!context.open) return null
|
|
223
|
-
if (portal && !mounted) return null
|
|
224
|
-
|
|
225
|
-
// Alignment classes are ignored if we use portal/fixed positioning,
|
|
226
|
-
// but kept for non-portal usage if someone opts out.
|
|
227
|
-
const alignmentClasses = {
|
|
228
|
-
start: 'left-0',
|
|
229
|
-
center: 'left-1/2 -translate-x-1/2',
|
|
230
|
-
end: 'right-0',
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const content = (
|
|
234
|
-
<div
|
|
235
|
-
ref={(node) => {
|
|
236
|
-
(contentRef as any).current = node
|
|
237
|
-
if (typeof ref === 'function') ref(node)
|
|
238
|
-
else if (ref) ref.current = node
|
|
239
|
-
}}
|
|
240
|
-
role="menu"
|
|
241
|
-
aria-orientation="vertical"
|
|
242
|
-
className={cn(
|
|
243
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
244
|
-
"animate-in fade-in-0 zoom-in-95",
|
|
245
|
-
!portal && "absolute", // Use absolute if not portal
|
|
246
|
-
!portal && alignmentClasses[align],
|
|
247
|
-
!portal && (side === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1'),
|
|
248
|
-
portal && "fixed", // Use fixed if portal
|
|
249
|
-
className
|
|
250
|
-
)}
|
|
251
|
-
style={{
|
|
252
|
-
top: portal ? position.top : undefined,
|
|
253
|
-
left: portal ? position.left : undefined,
|
|
254
|
-
...props.style
|
|
255
|
-
}}
|
|
256
|
-
{...props}
|
|
257
|
-
/>
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
if (portal) {
|
|
261
|
-
return createPortal(content, document.body)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return content
|
|
265
|
-
}
|
|
266
|
-
)
|
|
267
|
-
DropdownMenuContent.displayName = "DropdownMenuContent"
|
|
268
|
-
|
|
269
|
-
const DropdownMenuGroup = React.forwardRef<
|
|
270
|
-
HTMLDivElement,
|
|
271
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
272
|
-
>(({ className, ...props }, ref) => (
|
|
273
|
-
<div ref={ref} className={cn("", className)} {...props} />
|
|
274
|
-
))
|
|
275
|
-
DropdownMenuGroup.displayName = "DropdownMenuGroup"
|
|
276
|
-
|
|
277
|
-
const DropdownMenuPortal = ({ children }: { children: React.ReactNode }) => {
|
|
278
|
-
return <>{children}</>
|
|
279
|
-
}
|
|
280
|
-
DropdownMenuPortal.displayName = "DropdownMenuPortal"
|
|
281
|
-
|
|
282
|
-
const DropdownMenuItem = React.forwardRef<
|
|
283
|
-
HTMLDivElement,
|
|
284
|
-
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean; disabled?: boolean; asChild?: boolean; closeOnSelect?: boolean }
|
|
285
|
-
>(({ className, inset, disabled, onClick, asChild = false, closeOnSelect = true, ...props }, ref) => {
|
|
286
|
-
const context = React.useContext(DropdownMenuContext)
|
|
287
|
-
|
|
288
|
-
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
289
|
-
if (disabled) return
|
|
290
|
-
onClick?.(e)
|
|
291
|
-
if (closeOnSelect) {
|
|
292
|
-
context?.onOpenChange(false)
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (asChild) {
|
|
297
|
-
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
298
|
-
return React.cloneElement(child, {
|
|
299
|
-
ref,
|
|
300
|
-
className: cn(
|
|
301
|
-
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
302
|
-
inset && "pl-8",
|
|
303
|
-
disabled && "pointer-events-none opacity-50",
|
|
304
|
-
className,
|
|
305
|
-
child.props.className
|
|
306
|
-
),
|
|
307
|
-
onClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
|
308
|
-
handleClick(e)
|
|
309
|
-
child.props.onClick?.(e)
|
|
310
|
-
},
|
|
311
|
-
"data-disabled": disabled || undefined,
|
|
312
|
-
"data-inset": inset || undefined,
|
|
313
|
-
tabIndex: disabled ? -1 : 0,
|
|
314
|
-
...props,
|
|
315
|
-
children: child.props.children
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return (
|
|
320
|
-
<div
|
|
321
|
-
ref={ref}
|
|
322
|
-
role="menuitem"
|
|
323
|
-
tabIndex={disabled ? -1 : 0}
|
|
324
|
-
aria-disabled={disabled}
|
|
325
|
-
className={cn(
|
|
326
|
-
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer",
|
|
327
|
-
inset && "pl-8",
|
|
328
|
-
disabled && "pointer-events-none opacity-50",
|
|
329
|
-
className
|
|
330
|
-
)}
|
|
331
|
-
onClick={handleClick}
|
|
332
|
-
{...props}
|
|
333
|
-
/>
|
|
334
|
-
)
|
|
335
|
-
})
|
|
336
|
-
DropdownMenuItem.displayName = "DropdownMenuItem"
|
|
337
|
-
|
|
338
|
-
const DropdownMenuLabel = React.forwardRef<
|
|
339
|
-
HTMLDivElement,
|
|
340
|
-
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }
|
|
341
|
-
>(({ className, inset, ...props }, ref) => (
|
|
342
|
-
<div
|
|
343
|
-
ref={ref}
|
|
344
|
-
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
345
|
-
{...props}
|
|
346
|
-
/>
|
|
347
|
-
))
|
|
348
|
-
DropdownMenuLabel.displayName = "DropdownMenuLabel"
|
|
349
|
-
|
|
350
|
-
const DropdownMenuSeparator = React.forwardRef<
|
|
351
|
-
HTMLDivElement,
|
|
352
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
353
|
-
>(({ className, ...props }, ref) => (
|
|
354
|
-
<div ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
|
355
|
-
))
|
|
356
|
-
DropdownMenuSeparator.displayName = "DropdownMenuSeparator"
|
|
357
|
-
|
|
358
|
-
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
359
|
-
HTMLDivElement,
|
|
360
|
-
React.HTMLAttributes<HTMLDivElement> & { checked?: boolean; disabled?: boolean }
|
|
361
|
-
>(({ className, children, checked, disabled, onClick, ...props }, ref) => {
|
|
362
|
-
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
363
|
-
if (disabled) return
|
|
364
|
-
onClick?.(e)
|
|
365
|
-
// Checkbox items don't close the menu to allow multiple selections
|
|
366
|
-
e.preventDefault()
|
|
367
|
-
e.stopPropagation()
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return (
|
|
371
|
-
<div
|
|
372
|
-
ref={ref}
|
|
373
|
-
role="menuitemcheckbox"
|
|
374
|
-
aria-checked={checked}
|
|
375
|
-
aria-disabled={disabled}
|
|
376
|
-
tabIndex={disabled ? -1 : 0}
|
|
377
|
-
className={cn(
|
|
378
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
379
|
-
disabled && "pointer-events-none opacity-50",
|
|
380
|
-
className
|
|
381
|
-
)}
|
|
382
|
-
onClick={handleClick}
|
|
383
|
-
{...props}
|
|
384
|
-
>
|
|
385
|
-
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
386
|
-
{checked && (
|
|
387
|
-
<svg
|
|
388
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
389
|
-
viewBox="0 0 24 24"
|
|
390
|
-
fill="none"
|
|
391
|
-
stroke="currentColor"
|
|
392
|
-
strokeWidth="2"
|
|
393
|
-
strokeLinecap="round"
|
|
394
|
-
strokeLinejoin="round"
|
|
395
|
-
className="h-4 w-4"
|
|
396
|
-
>
|
|
397
|
-
<polyline points="20 6 9 17 4 12" />
|
|
398
|
-
</svg>
|
|
399
|
-
)}
|
|
400
|
-
</span>
|
|
401
|
-
{children}
|
|
402
|
-
</div>
|
|
403
|
-
)
|
|
404
|
-
})
|
|
405
|
-
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"
|
|
406
|
-
|
|
407
|
-
const DropdownMenuRadioGroup = React.forwardRef<
|
|
408
|
-
HTMLDivElement,
|
|
409
|
-
React.HTMLAttributes<HTMLDivElement> & { value?: string; onValueChange?: (value: string) => void }
|
|
410
|
-
>(({ className, children, ...props }, ref) => {
|
|
411
|
-
return (
|
|
412
|
-
<div ref={ref} role="group" className={className} {...props}>
|
|
413
|
-
{children}
|
|
414
|
-
</div>
|
|
415
|
-
)
|
|
416
|
-
})
|
|
417
|
-
DropdownMenuRadioGroup.displayName = "DropdownMenuRadioGroup"
|
|
418
|
-
|
|
419
|
-
const DropdownMenuRadioItem = React.forwardRef<
|
|
420
|
-
HTMLDivElement,
|
|
421
|
-
React.HTMLAttributes<HTMLDivElement> & { value: string; disabled?: boolean }
|
|
422
|
-
>(({ className, children, value, disabled, onClick, ...props }, ref) => {
|
|
423
|
-
const context = React.useContext(DropdownMenuContext)
|
|
424
|
-
|
|
425
|
-
return (
|
|
426
|
-
<div
|
|
427
|
-
ref={ref}
|
|
428
|
-
role="menuitemradio"
|
|
429
|
-
aria-disabled={disabled}
|
|
430
|
-
tabIndex={disabled ? -1 : 0}
|
|
431
|
-
className={cn(
|
|
432
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
433
|
-
disabled && "pointer-events-none opacity-50",
|
|
434
|
-
className
|
|
435
|
-
)}
|
|
436
|
-
onClick={(e) => {
|
|
437
|
-
if (disabled) return
|
|
438
|
-
onClick?.(e)
|
|
439
|
-
context?.onOpenChange(false)
|
|
440
|
-
}}
|
|
441
|
-
{...props}
|
|
442
|
-
>
|
|
443
|
-
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
444
|
-
<svg
|
|
445
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
446
|
-
viewBox="0 0 24 24"
|
|
447
|
-
fill="currentColor"
|
|
448
|
-
className="h-2 w-2 fill-current"
|
|
449
|
-
>
|
|
450
|
-
<circle cx="12" cy="12" r="10" />
|
|
451
|
-
</svg>
|
|
452
|
-
</span>
|
|
453
|
-
{children}
|
|
454
|
-
</div>
|
|
455
|
-
)
|
|
456
|
-
})
|
|
457
|
-
DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem"
|
|
458
|
-
|
|
459
|
-
const DropdownMenuSub = React.forwardRef<
|
|
460
|
-
HTMLDivElement,
|
|
461
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
462
|
-
>(({ ...props }, ref) => (
|
|
463
|
-
<div ref={ref} {...props} />
|
|
464
|
-
))
|
|
465
|
-
DropdownMenuSub.displayName = "DropdownMenuSub"
|
|
466
|
-
|
|
467
|
-
const DropdownMenuSubTrigger = React.forwardRef<
|
|
468
|
-
HTMLDivElement,
|
|
469
|
-
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }
|
|
470
|
-
>(({ className, inset, children, ...props }, ref) => (
|
|
471
|
-
<div
|
|
472
|
-
ref={ref}
|
|
473
|
-
className={cn(
|
|
474
|
-
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
|
475
|
-
inset && "pl-8",
|
|
476
|
-
className
|
|
477
|
-
)}
|
|
478
|
-
{...props}
|
|
479
|
-
>
|
|
480
|
-
{children}
|
|
481
|
-
<svg
|
|
482
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
483
|
-
width="24"
|
|
484
|
-
height="24"
|
|
485
|
-
viewBox="0 0 24 24"
|
|
486
|
-
fill="none"
|
|
487
|
-
stroke="currentColor"
|
|
488
|
-
strokeWidth="2"
|
|
489
|
-
strokeLinecap="round"
|
|
490
|
-
strokeLinejoin="round"
|
|
491
|
-
className="ml-auto h-4 w-4"
|
|
492
|
-
>
|
|
493
|
-
<path d="m9 18 6-6-6-6" />
|
|
494
|
-
</svg>
|
|
495
|
-
</div>
|
|
496
|
-
))
|
|
497
|
-
DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger"
|
|
498
|
-
|
|
499
|
-
const DropdownMenuSubContent = React.forwardRef<
|
|
500
|
-
HTMLDivElement,
|
|
501
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
502
|
-
>(({ className, ...props }, ref) => (
|
|
503
|
-
<div
|
|
504
|
-
ref={ref}
|
|
505
|
-
className={cn(
|
|
506
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
507
|
-
className
|
|
508
|
-
)}
|
|
509
|
-
{...props}
|
|
510
|
-
/>
|
|
511
|
-
))
|
|
512
|
-
DropdownMenuSubContent.displayName = "DropdownMenuSubContent"
|
|
513
|
-
|
|
514
|
-
export {
|
|
515
|
-
DropdownMenu,
|
|
516
|
-
DropdownMenuTrigger,
|
|
517
|
-
DropdownMenuContent,
|
|
518
|
-
DropdownMenuItem,
|
|
519
|
-
DropdownMenuCheckboxItem,
|
|
520
|
-
DropdownMenuRadioItem,
|
|
521
|
-
DropdownMenuRadioGroup,
|
|
522
|
-
DropdownMenuLabel,
|
|
523
|
-
DropdownMenuSeparator,
|
|
524
|
-
DropdownMenuSub,
|
|
525
|
-
DropdownMenuSubTrigger,
|
|
526
|
-
DropdownMenuSubContent,
|
|
527
|
-
DropdownMenuGroup,
|
|
528
|
-
DropdownMenuPortal,
|
|
529
|
-
}
|
|
530
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { createPortal } from "react-dom"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Slot } from "@/components/ui/slot"
|
|
7
|
+
|
|
8
|
+
interface DropdownMenuContextValue {
|
|
9
|
+
open: boolean
|
|
10
|
+
onOpenChange: (open: boolean) => void
|
|
11
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null)
|
|
15
|
+
|
|
16
|
+
interface DropdownMenuProps {
|
|
17
|
+
children: React.ReactNode
|
|
18
|
+
open?: boolean
|
|
19
|
+
onOpenChange?: (open: boolean) => void
|
|
20
|
+
defaultOpen?: boolean
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* DropdownMenu component with keyboard navigation and proper positioning
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* <DropdownMenu>
|
|
29
|
+
* <DropdownMenuTrigger asChild>
|
|
30
|
+
* <Button>Open Menu</Button>
|
|
31
|
+
* </DropdownMenuTrigger>
|
|
32
|
+
* <DropdownMenuContent>
|
|
33
|
+
* <DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
34
|
+
* <DropdownMenuSeparator />
|
|
35
|
+
* <DropdownMenuItem>Profile</DropdownMenuItem>
|
|
36
|
+
* <DropdownMenuItem>Settings</DropdownMenuItem>
|
|
37
|
+
* </DropdownMenuContent>
|
|
38
|
+
* </DropdownMenu>
|
|
39
|
+
*/
|
|
40
|
+
function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpen = false, className }: DropdownMenuProps) {
|
|
41
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
42
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
43
|
+
|
|
44
|
+
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
45
|
+
const setOpen = onOpenChange || setUncontrolledOpen
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
|
|
49
|
+
<div className={cn("relative inline-block text-left", className)}>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
</DropdownMenuContext.Provider>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
interface DropdownMenuTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
58
|
+
asChild?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DropdownMenuTrigger = React.forwardRef<HTMLButtonElement, DropdownMenuTriggerProps>(
|
|
62
|
+
({ onClick, asChild, children, ...props }, ref) => {
|
|
63
|
+
const context = React.useContext(DropdownMenuContext)
|
|
64
|
+
if (!context) throw new Error("DropdownMenuTrigger must be used within DropdownMenu")
|
|
65
|
+
|
|
66
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
67
|
+
onClick?.(e)
|
|
68
|
+
context.onOpenChange(!context.open)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Combine refs
|
|
72
|
+
const combinedRef = (node: HTMLButtonElement | null) => {
|
|
73
|
+
(context.triggerRef as any).current = node
|
|
74
|
+
if (typeof ref === 'function') ref(node)
|
|
75
|
+
else if (ref) ref.current = node
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (asChild) {
|
|
79
|
+
// For asChild, we need to manually make sure the ref is passed correctly.
|
|
80
|
+
// Slot handles merging the passed ref with the child's ref.
|
|
81
|
+
// But here we're passing `combinedRef` as the ref to Slot.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const Comp = asChild ? Slot : "button"
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Comp
|
|
88
|
+
ref={combinedRef}
|
|
89
|
+
aria-expanded={context.open}
|
|
90
|
+
aria-haspopup="menu"
|
|
91
|
+
onClick={handleClick}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</Comp>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
|
|
100
|
+
|
|
101
|
+
interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
102
|
+
/** Alignment relative to trigger: 'start' | 'center' | 'end' */
|
|
103
|
+
align?: 'start' | 'center' | 'end'
|
|
104
|
+
/** Side of trigger to open: 'bottom' | 'top' */
|
|
105
|
+
side?: 'bottom' | 'top'
|
|
106
|
+
/** Offset from trigger in pixels */
|
|
107
|
+
sideOffset?: number
|
|
108
|
+
/** Whether to render in a portal (default: true) */
|
|
109
|
+
portal?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
|
113
|
+
({ className, align = 'start', side = 'bottom', sideOffset = 4, portal = true, ...props }, ref) => {
|
|
114
|
+
const context = React.useContext(DropdownMenuContext)
|
|
115
|
+
if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
|
|
116
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
117
|
+
const [position, setPosition] = React.useState({ top: 0, left: 0 })
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
119
|
+
const [currentSide, setCurrentSide] = React.useState(side)
|
|
120
|
+
|
|
121
|
+
// Reset scroll on mount/unmount if needed, but mainly we just need a portal container
|
|
122
|
+
const [mounted, setMounted] = React.useState(false)
|
|
123
|
+
React.useEffect(() => {
|
|
124
|
+
setMounted(true)
|
|
125
|
+
}, [])
|
|
126
|
+
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
129
|
+
if (context.open) {
|
|
130
|
+
const target = e.target as Node
|
|
131
|
+
const content = contentRef.current
|
|
132
|
+
const trigger = context.triggerRef.current
|
|
133
|
+
|
|
134
|
+
// Don't close if clicking inside content or trigger
|
|
135
|
+
if (content?.contains(target) || trigger?.contains(target)) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
context.onOpenChange(false)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
143
|
+
if (e.key === "Escape" && context.open) {
|
|
144
|
+
context.onOpenChange(false)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const checkPosition = () => {
|
|
149
|
+
if (context.open && contentRef.current && context.triggerRef.current) {
|
|
150
|
+
const triggerRect = context.triggerRef.current.getBoundingClientRect()
|
|
151
|
+
const contentRect = contentRef.current.getBoundingClientRect()
|
|
152
|
+
const viewportHeight = window.innerHeight
|
|
153
|
+
const viewportWidth = window.innerWidth
|
|
154
|
+
|
|
155
|
+
let top = 0
|
|
156
|
+
let left = 0
|
|
157
|
+
let usedSide = side
|
|
158
|
+
|
|
159
|
+
// Vertical positioning logic
|
|
160
|
+
const spaceBelow = viewportHeight - triggerRect.bottom
|
|
161
|
+
const spaceAbove = triggerRect.top
|
|
162
|
+
const neededHeight = contentRect.height + sideOffset
|
|
163
|
+
|
|
164
|
+
if (side === 'bottom') {
|
|
165
|
+
if (spaceBelow < neededHeight && spaceAbove > neededHeight) {
|
|
166
|
+
usedSide = 'top'
|
|
167
|
+
}
|
|
168
|
+
} else if (side === 'top') {
|
|
169
|
+
if (spaceAbove < neededHeight && spaceBelow > neededHeight) {
|
|
170
|
+
usedSide = 'bottom'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
setCurrentSide(usedSide)
|
|
174
|
+
|
|
175
|
+
if (usedSide === 'bottom') {
|
|
176
|
+
top = triggerRect.bottom + sideOffset
|
|
177
|
+
} else {
|
|
178
|
+
top = triggerRect.top - contentRect.height - sideOffset
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Horizontal positioning logic (Alignment)
|
|
182
|
+
if (align === 'start') {
|
|
183
|
+
left = triggerRect.left
|
|
184
|
+
} else if (align === 'end') {
|
|
185
|
+
left = triggerRect.right - contentRect.width
|
|
186
|
+
} else {
|
|
187
|
+
// center
|
|
188
|
+
left = triggerRect.left + (triggerRect.width - contentRect.width) / 2
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Collision detection / clamping for X axis
|
|
192
|
+
if (left < 4) left = 4
|
|
193
|
+
if (left + contentRect.width > viewportWidth - 4) {
|
|
194
|
+
left = viewportWidth - contentRect.width - 4
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setPosition({ top, left })
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (context.open) {
|
|
202
|
+
// Check position immediately after render cycle
|
|
203
|
+
requestAnimationFrame(checkPosition)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
document.addEventListener("click", handleClickOutside)
|
|
208
|
+
}, 0)
|
|
209
|
+
document.addEventListener("keydown", handleEscape)
|
|
210
|
+
window.addEventListener("resize", checkPosition)
|
|
211
|
+
window.addEventListener("scroll", checkPosition, true)
|
|
212
|
+
|
|
213
|
+
return () => {
|
|
214
|
+
clearTimeout(timer)
|
|
215
|
+
document.removeEventListener("click", handleClickOutside)
|
|
216
|
+
document.removeEventListener("keydown", handleEscape)
|
|
217
|
+
window.removeEventListener("resize", checkPosition)
|
|
218
|
+
window.removeEventListener("scroll", checkPosition, true)
|
|
219
|
+
}
|
|
220
|
+
}, [context.open, context, side, sideOffset, align])
|
|
221
|
+
|
|
222
|
+
if (!context.open) return null
|
|
223
|
+
if (portal && !mounted) return null
|
|
224
|
+
|
|
225
|
+
// Alignment classes are ignored if we use portal/fixed positioning,
|
|
226
|
+
// but kept for non-portal usage if someone opts out.
|
|
227
|
+
const alignmentClasses = {
|
|
228
|
+
start: 'left-0',
|
|
229
|
+
center: 'left-1/2 -translate-x-1/2',
|
|
230
|
+
end: 'right-0',
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const content = (
|
|
234
|
+
<div
|
|
235
|
+
ref={(node) => {
|
|
236
|
+
(contentRef as any).current = node
|
|
237
|
+
if (typeof ref === 'function') ref(node)
|
|
238
|
+
else if (ref) ref.current = node
|
|
239
|
+
}}
|
|
240
|
+
role="menu"
|
|
241
|
+
aria-orientation="vertical"
|
|
242
|
+
className={cn(
|
|
243
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
244
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
245
|
+
!portal && "absolute", // Use absolute if not portal
|
|
246
|
+
!portal && alignmentClasses[align],
|
|
247
|
+
!portal && (side === 'bottom' ? 'top-full mt-1' : 'bottom-full mb-1'),
|
|
248
|
+
portal && "fixed", // Use fixed if portal
|
|
249
|
+
className
|
|
250
|
+
)}
|
|
251
|
+
style={{
|
|
252
|
+
top: portal ? position.top : undefined,
|
|
253
|
+
left: portal ? position.left : undefined,
|
|
254
|
+
...props.style
|
|
255
|
+
}}
|
|
256
|
+
{...props}
|
|
257
|
+
/>
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if (portal) {
|
|
261
|
+
return createPortal(content, document.body)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return content
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
DropdownMenuContent.displayName = "DropdownMenuContent"
|
|
268
|
+
|
|
269
|
+
const DropdownMenuGroup = React.forwardRef<
|
|
270
|
+
HTMLDivElement,
|
|
271
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
272
|
+
>(({ className, ...props }, ref) => (
|
|
273
|
+
<div ref={ref} className={cn("", className)} {...props} />
|
|
274
|
+
))
|
|
275
|
+
DropdownMenuGroup.displayName = "DropdownMenuGroup"
|
|
276
|
+
|
|
277
|
+
const DropdownMenuPortal = ({ children }: { children: React.ReactNode }) => {
|
|
278
|
+
return <>{children}</>
|
|
279
|
+
}
|
|
280
|
+
DropdownMenuPortal.displayName = "DropdownMenuPortal"
|
|
281
|
+
|
|
282
|
+
const DropdownMenuItem = React.forwardRef<
|
|
283
|
+
HTMLDivElement,
|
|
284
|
+
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean; disabled?: boolean; asChild?: boolean; closeOnSelect?: boolean }
|
|
285
|
+
>(({ className, inset, disabled, onClick, asChild = false, closeOnSelect = true, ...props }, ref) => {
|
|
286
|
+
const context = React.useContext(DropdownMenuContext)
|
|
287
|
+
|
|
288
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
289
|
+
if (disabled) return
|
|
290
|
+
onClick?.(e)
|
|
291
|
+
if (closeOnSelect) {
|
|
292
|
+
context?.onOpenChange(false)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (asChild) {
|
|
297
|
+
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
298
|
+
return React.cloneElement(child, {
|
|
299
|
+
ref,
|
|
300
|
+
className: cn(
|
|
301
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
302
|
+
inset && "pl-8",
|
|
303
|
+
disabled && "pointer-events-none opacity-50",
|
|
304
|
+
className,
|
|
305
|
+
child.props.className
|
|
306
|
+
),
|
|
307
|
+
onClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
|
308
|
+
handleClick(e)
|
|
309
|
+
child.props.onClick?.(e)
|
|
310
|
+
},
|
|
311
|
+
"data-disabled": disabled || undefined,
|
|
312
|
+
"data-inset": inset || undefined,
|
|
313
|
+
tabIndex: disabled ? -1 : 0,
|
|
314
|
+
...props,
|
|
315
|
+
children: child.props.children
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div
|
|
321
|
+
ref={ref}
|
|
322
|
+
role="menuitem"
|
|
323
|
+
tabIndex={disabled ? -1 : 0}
|
|
324
|
+
aria-disabled={disabled}
|
|
325
|
+
className={cn(
|
|
326
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer",
|
|
327
|
+
inset && "pl-8",
|
|
328
|
+
disabled && "pointer-events-none opacity-50",
|
|
329
|
+
className
|
|
330
|
+
)}
|
|
331
|
+
onClick={handleClick}
|
|
332
|
+
{...props}
|
|
333
|
+
/>
|
|
334
|
+
)
|
|
335
|
+
})
|
|
336
|
+
DropdownMenuItem.displayName = "DropdownMenuItem"
|
|
337
|
+
|
|
338
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
339
|
+
HTMLDivElement,
|
|
340
|
+
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }
|
|
341
|
+
>(({ className, inset, ...props }, ref) => (
|
|
342
|
+
<div
|
|
343
|
+
ref={ref}
|
|
344
|
+
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
345
|
+
{...props}
|
|
346
|
+
/>
|
|
347
|
+
))
|
|
348
|
+
DropdownMenuLabel.displayName = "DropdownMenuLabel"
|
|
349
|
+
|
|
350
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
351
|
+
HTMLDivElement,
|
|
352
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
353
|
+
>(({ className, ...props }, ref) => (
|
|
354
|
+
<div ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
|
355
|
+
))
|
|
356
|
+
DropdownMenuSeparator.displayName = "DropdownMenuSeparator"
|
|
357
|
+
|
|
358
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
359
|
+
HTMLDivElement,
|
|
360
|
+
React.HTMLAttributes<HTMLDivElement> & { checked?: boolean; disabled?: boolean }
|
|
361
|
+
>(({ className, children, checked, disabled, onClick, ...props }, ref) => {
|
|
362
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
363
|
+
if (disabled) return
|
|
364
|
+
onClick?.(e)
|
|
365
|
+
// Checkbox items don't close the menu to allow multiple selections
|
|
366
|
+
e.preventDefault()
|
|
367
|
+
e.stopPropagation()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<div
|
|
372
|
+
ref={ref}
|
|
373
|
+
role="menuitemcheckbox"
|
|
374
|
+
aria-checked={checked}
|
|
375
|
+
aria-disabled={disabled}
|
|
376
|
+
tabIndex={disabled ? -1 : 0}
|
|
377
|
+
className={cn(
|
|
378
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
379
|
+
disabled && "pointer-events-none opacity-50",
|
|
380
|
+
className
|
|
381
|
+
)}
|
|
382
|
+
onClick={handleClick}
|
|
383
|
+
{...props}
|
|
384
|
+
>
|
|
385
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
386
|
+
{checked && (
|
|
387
|
+
<svg
|
|
388
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
389
|
+
viewBox="0 0 24 24"
|
|
390
|
+
fill="none"
|
|
391
|
+
stroke="currentColor"
|
|
392
|
+
strokeWidth="2"
|
|
393
|
+
strokeLinecap="round"
|
|
394
|
+
strokeLinejoin="round"
|
|
395
|
+
className="h-4 w-4"
|
|
396
|
+
>
|
|
397
|
+
<polyline points="20 6 9 17 4 12" />
|
|
398
|
+
</svg>
|
|
399
|
+
)}
|
|
400
|
+
</span>
|
|
401
|
+
{children}
|
|
402
|
+
</div>
|
|
403
|
+
)
|
|
404
|
+
})
|
|
405
|
+
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"
|
|
406
|
+
|
|
407
|
+
const DropdownMenuRadioGroup = React.forwardRef<
|
|
408
|
+
HTMLDivElement,
|
|
409
|
+
React.HTMLAttributes<HTMLDivElement> & { value?: string; onValueChange?: (value: string) => void }
|
|
410
|
+
>(({ className, children, ...props }, ref) => {
|
|
411
|
+
return (
|
|
412
|
+
<div ref={ref} role="group" className={className} {...props}>
|
|
413
|
+
{children}
|
|
414
|
+
</div>
|
|
415
|
+
)
|
|
416
|
+
})
|
|
417
|
+
DropdownMenuRadioGroup.displayName = "DropdownMenuRadioGroup"
|
|
418
|
+
|
|
419
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
420
|
+
HTMLDivElement,
|
|
421
|
+
React.HTMLAttributes<HTMLDivElement> & { value: string; disabled?: boolean }
|
|
422
|
+
>(({ className, children, value, disabled, onClick, ...props }, ref) => {
|
|
423
|
+
const context = React.useContext(DropdownMenuContext)
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<div
|
|
427
|
+
ref={ref}
|
|
428
|
+
role="menuitemradio"
|
|
429
|
+
aria-disabled={disabled}
|
|
430
|
+
tabIndex={disabled ? -1 : 0}
|
|
431
|
+
className={cn(
|
|
432
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
433
|
+
disabled && "pointer-events-none opacity-50",
|
|
434
|
+
className
|
|
435
|
+
)}
|
|
436
|
+
onClick={(e) => {
|
|
437
|
+
if (disabled) return
|
|
438
|
+
onClick?.(e)
|
|
439
|
+
context?.onOpenChange(false)
|
|
440
|
+
}}
|
|
441
|
+
{...props}
|
|
442
|
+
>
|
|
443
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
444
|
+
<svg
|
|
445
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
446
|
+
viewBox="0 0 24 24"
|
|
447
|
+
fill="currentColor"
|
|
448
|
+
className="h-2 w-2 fill-current"
|
|
449
|
+
>
|
|
450
|
+
<circle cx="12" cy="12" r="10" />
|
|
451
|
+
</svg>
|
|
452
|
+
</span>
|
|
453
|
+
{children}
|
|
454
|
+
</div>
|
|
455
|
+
)
|
|
456
|
+
})
|
|
457
|
+
DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem"
|
|
458
|
+
|
|
459
|
+
const DropdownMenuSub = React.forwardRef<
|
|
460
|
+
HTMLDivElement,
|
|
461
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
462
|
+
>(({ ...props }, ref) => (
|
|
463
|
+
<div ref={ref} {...props} />
|
|
464
|
+
))
|
|
465
|
+
DropdownMenuSub.displayName = "DropdownMenuSub"
|
|
466
|
+
|
|
467
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
468
|
+
HTMLDivElement,
|
|
469
|
+
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }
|
|
470
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
471
|
+
<div
|
|
472
|
+
ref={ref}
|
|
473
|
+
className={cn(
|
|
474
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
|
475
|
+
inset && "pl-8",
|
|
476
|
+
className
|
|
477
|
+
)}
|
|
478
|
+
{...props}
|
|
479
|
+
>
|
|
480
|
+
{children}
|
|
481
|
+
<svg
|
|
482
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
483
|
+
width="24"
|
|
484
|
+
height="24"
|
|
485
|
+
viewBox="0 0 24 24"
|
|
486
|
+
fill="none"
|
|
487
|
+
stroke="currentColor"
|
|
488
|
+
strokeWidth="2"
|
|
489
|
+
strokeLinecap="round"
|
|
490
|
+
strokeLinejoin="round"
|
|
491
|
+
className="ml-auto h-4 w-4"
|
|
492
|
+
>
|
|
493
|
+
<path d="m9 18 6-6-6-6" />
|
|
494
|
+
</svg>
|
|
495
|
+
</div>
|
|
496
|
+
))
|
|
497
|
+
DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger"
|
|
498
|
+
|
|
499
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
500
|
+
HTMLDivElement,
|
|
501
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
502
|
+
>(({ className, ...props }, ref) => (
|
|
503
|
+
<div
|
|
504
|
+
ref={ref}
|
|
505
|
+
className={cn(
|
|
506
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
507
|
+
className
|
|
508
|
+
)}
|
|
509
|
+
{...props}
|
|
510
|
+
/>
|
|
511
|
+
))
|
|
512
|
+
DropdownMenuSubContent.displayName = "DropdownMenuSubContent"
|
|
513
|
+
|
|
514
|
+
export {
|
|
515
|
+
DropdownMenu,
|
|
516
|
+
DropdownMenuTrigger,
|
|
517
|
+
DropdownMenuContent,
|
|
518
|
+
DropdownMenuItem,
|
|
519
|
+
DropdownMenuCheckboxItem,
|
|
520
|
+
DropdownMenuRadioItem,
|
|
521
|
+
DropdownMenuRadioGroup,
|
|
522
|
+
DropdownMenuLabel,
|
|
523
|
+
DropdownMenuSeparator,
|
|
524
|
+
DropdownMenuSub,
|
|
525
|
+
DropdownMenuSubTrigger,
|
|
526
|
+
DropdownMenuSubContent,
|
|
527
|
+
DropdownMenuGroup,
|
|
528
|
+
DropdownMenuPortal,
|
|
529
|
+
}
|