Initial commit

This commit is contained in:
Rami Bitar
2026-04-19 11:15:55 -04:00
commit eeeafd36d3
78 changed files with 10412 additions and 0 deletions

View File

@@ -0,0 +1,616 @@
"use client"
import * as React from "react"
import { createPortal } from "react-dom"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
interface DropdownMenuContextValue {
open: boolean
setOpen: (open: boolean) => void
triggerRef: React.RefObject<HTMLButtonElement | null>
}
const DropdownMenuContext = React.createContext<DropdownMenuContextValue | null>(null)
function useDropdownMenu() {
const context = React.useContext(DropdownMenuContext)
if (!context) {
throw new Error("useDropdownMenu must be used within a DropdownMenu")
}
return context
}
interface DropdownMenuProps {
children: React.ReactNode
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
}
function DropdownMenu({
children,
open: controlledOpen,
defaultOpen = false,
onOpenChange,
}: DropdownMenuProps) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const triggerRef = React.useRef<HTMLButtonElement>(null)
const isControlled = controlledOpen !== undefined
const open = isControlled ? controlledOpen : uncontrolledOpen
const setOpen = React.useCallback((value: boolean) => {
if (!isControlled) {
setUncontrolledOpen(value)
}
onOpenChange?.(value)
}, [isControlled, onOpenChange])
return (
<DropdownMenuContext.Provider value={{ open, setOpen, triggerRef }}>
{children}
</DropdownMenuContext.Provider>
)
}
function DropdownMenuTrigger({
className,
children,
asChild,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean }) {
const { open, setOpen, triggerRef } = useDropdownMenu()
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
setOpen(!open)
props.onClick?.(e)
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
e.preventDefault()
setOpen(true)
}
props.onKeyDown?.(e)
}
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children as React.ReactElement<any>, {
ref: triggerRef,
onClick: handleClick,
onKeyDown: handleKeyDown,
"aria-expanded": open,
"aria-haspopup": "menu",
"data-state": open ? "open" : "closed",
})
}
return (
<button
ref={triggerRef}
type="button"
data-slot="dropdown-menu-trigger"
aria-expanded={open}
aria-haspopup="menu"
data-state={open ? "open" : "closed"}
className={cn("relative", className)}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...props}
>
{children}
</button>
)
}
function DropdownMenuPortal({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
sideOffset?: number
align?: "start" | "center" | "end"
side?: "top" | "right" | "bottom" | "left"
}
function DropdownMenuContent({
className,
sideOffset = 4,
align = "start",
side = "bottom",
children,
...props
}: DropdownMenuContentProps) {
const { open, setOpen, triggerRef } = useDropdownMenu()
const contentRef = React.useRef<HTMLDivElement>(null)
const [position, setPosition] = React.useState({ top: 0, left: 0 })
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
React.useLayoutEffect(() => {
if (!open || !triggerRef.current || !contentRef.current) return
const trigger = triggerRef.current.getBoundingClientRect()
const content = contentRef.current.getBoundingClientRect()
let top = 0
let left = 0
// Calculate position based on side
switch (side) {
case "top":
top = trigger.top - content.height - sideOffset
break
case "bottom":
top = trigger.bottom + sideOffset
break
case "left":
left = trigger.left - content.width - sideOffset
top = trigger.top
break
case "right":
left = trigger.right + sideOffset
top = trigger.top
break
}
// Calculate alignment for top/bottom
if (side === "top" || side === "bottom") {
switch (align) {
case "start":
left = trigger.left
break
case "center":
left = trigger.left + (trigger.width - content.width) / 2
break
case "end":
left = trigger.right - content.width
break
}
}
// Calculate alignment for left/right
if (side === "left" || side === "right") {
switch (align) {
case "start":
top = trigger.top
break
case "center":
top = trigger.top + (trigger.height - content.height) / 2
break
case "end":
top = trigger.bottom - content.height
break
}
}
setPosition({ top, left })
}, [open, side, align, sideOffset, triggerRef])
React.useEffect(() => {
if (!open) return
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node
if (
contentRef.current &&
!contentRef.current.contains(target) &&
triggerRef.current &&
!triggerRef.current.contains(target)
) {
setOpen(false)
}
}
const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false)
triggerRef.current?.focus()
}
}
document.addEventListener("mousedown", handleClickOutside)
document.addEventListener("keydown", handleEscape)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
document.removeEventListener("keydown", handleEscape)
}
}, [open, setOpen, triggerRef])
if (!open || !mounted) return null
const slideClasses = {
top: "slide-in-from-bottom-2",
bottom: "slide-in-from-top-2",
left: "slide-in-from-right-2",
right: "slide-in-from-left-2",
}
return createPortal(
<div
ref={contentRef}
data-slot="dropdown-menu-content"
data-state={open ? "open" : "closed"}
role="menu"
className={cn(
"fixed z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"animate-in fade-in-0 zoom-in-95",
slideClasses[side],
className
)}
style={{
top: position.top,
left: position.left,
}}
{...props}
>
{children}
</div>,
document.body
)
}
function DropdownMenuGroup({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="dropdown-menu-group"
role="group"
className={className}
{...props}
/>
)
}
interface DropdownMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
inset?: boolean
variant?: "default" | "destructive"
asChild?: boolean
}
function DropdownMenuItem({
className,
inset,
variant = "default",
children,
asChild,
...props
}: DropdownMenuItemProps) {
const { setOpen } = useDropdownMenu()
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
props.onClick?.(e)
if (!e.defaultPrevented) {
setOpen(false)
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
handleClick(e as unknown as React.MouseEvent<HTMLButtonElement>)
}
props.onKeyDown?.(e)
}
const itemClasses = cn(
"focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variant === "destructive" && "text-destructive focus:bg-destructive/10 focus:text-destructive dark:focus:bg-destructive/20",
inset && "pl-8",
className
)
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children as React.ReactElement<any>, {
className: itemClasses,
role: "menuitem",
tabIndex: -1,
onClick: handleClick,
onKeyDown: handleKeyDown,
})
}
return (
<button
type="button"
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
role="menuitem"
tabIndex={-1}
className={itemClasses}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...props}
>
{children}
</button>
)
}
interface DropdownMenuCheckboxItemProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "checked"> {
checked?: boolean
onCheckedChange?: (checked: boolean) => void
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
onCheckedChange,
...props
}: DropdownMenuCheckboxItemProps) {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
onCheckedChange?.(!checked)
props.onClick?.(e)
}
return (
<button
type="button"
data-slot="dropdown-menu-checkbox-item"
role="menuitemcheckbox"
aria-checked={checked}
tabIndex={-1}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
onClick={handleClick}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{checked && <CheckIcon className="size-4" />}
</span>
{children}
</button>
)
}
interface DropdownMenuRadioGroupProps extends React.HTMLAttributes<HTMLDivElement> {
value?: string
onValueChange?: (value: string) => void
}
const RadioGroupContext = React.createContext<{
value?: string
onValueChange?: (value: string) => void
} | null>(null)
function DropdownMenuRadioGroup({
value,
onValueChange,
children,
...props
}: DropdownMenuRadioGroupProps) {
return (
<RadioGroupContext.Provider value={{ value, onValueChange }}>
<div
data-slot="dropdown-menu-radio-group"
role="group"
{...props}
>
{children}
</div>
</RadioGroupContext.Provider>
)
}
interface DropdownMenuRadioItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string
}
function DropdownMenuRadioItem({
className,
children,
value,
...props
}: DropdownMenuRadioItemProps) {
const context = React.useContext(RadioGroupContext)
const checked = context?.value === value
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
context?.onValueChange?.(value)
props.onClick?.(e)
}
return (
<button
type="button"
data-slot="dropdown-menu-radio-item"
role="menuitemradio"
aria-checked={checked}
tabIndex={-1}
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
onClick={handleClick}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{checked && <CircleIcon className="size-2 fill-current" />}
</span>
{children}
</button>
)
}
interface DropdownMenuLabelProps extends React.HTMLAttributes<HTMLDivElement> {
inset?: boolean
}
function DropdownMenuLabel({
className,
inset,
...props
}: DropdownMenuLabelProps) {
return (
<div
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium",
inset && "pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="dropdown-menu-separator"
role="separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
interface SubMenuContextValue {
open: boolean
setOpen: (open: boolean) => void
}
const SubMenuContext = React.createContext<SubMenuContextValue | null>(null)
function DropdownMenuSub({ children }: { children: React.ReactNode }) {
const [open, setOpen] = React.useState(false)
return (
<SubMenuContext.Provider value={{ open, setOpen }}>
<div data-slot="dropdown-menu-sub" className="relative">
{children}
</div>
</SubMenuContext.Provider>
)
}
interface DropdownMenuSubTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
inset?: boolean
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: DropdownMenuSubTriggerProps) {
const context = React.useContext(SubMenuContext)
const handleMouseEnter = () => {
context?.setOpen(true)
}
const handleMouseLeave = () => {
context?.setOpen(false)
}
return (
<button
type="button"
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
data-state={context?.open ? "open" : "closed"}
role="menuitem"
aria-haspopup="menu"
aria-expanded={context?.open}
tabIndex={-1}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
"[&_svg:not([class*='text-'])]:text-muted-foreground flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none",
inset && "pl-8",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</button>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
const context = React.useContext(SubMenuContext)
if (!context?.open) return null
return (
<div
data-slot="dropdown-menu-sub-content"
data-state={context.open ? "open" : "closed"}
role="menu"
className={cn(
"absolute left-full top-0 z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
"animate-in fade-in-0 zoom-in-95 slide-in-from-left-2",
className
)}
onMouseEnter={() => context.setOpen(true)}
onMouseLeave={() => context.setOpen(false)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}