import React, { useEffect, useRef, useState } from 'react'; import { clsx } from 'clsx'; import { ChevronDown } from 'lucide-react'; export interface DropdownMenuItem { label: string; value: string; icon?: React.ReactNode; disabled?: boolean; divider?: boolean; onClick?: () => void; } export interface DropdownMenuProps { trigger: React.ReactNode; items: DropdownMenuItem[]; onSelect?: (value: string) => void; align?: 'left' | 'right' | 'center'; position?: 'top' | 'bottom'; className?: string; triggerClassName?: string; } /** * Dropdown menu component */ export const DropdownMenu = ({ trigger, items, onSelect, align = 'left', position = 'bottom', className, triggerClassName, }: DropdownMenuProps) => { const [isOpen, setIsOpen] = useState(false); const menuRef = useRef(null); const triggerRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( menuRef.current && triggerRef.current && !menuRef.current.contains(event.target as Node) && !triggerRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleEscape); } return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); }; }, [isOpen]); const alignClasses = { left: 'left-0', right: 'right-0', center: 'left-1/2 -translate-x-1/2', }; const positionClasses = { top: 'bottom-full mb-2', bottom: 'top-full mt-2', }; const handleItemClick = (item: DropdownMenuItem) => { if (item.disabled) return; item.onClick?.(); onSelect?.(item.value); setIsOpen(false); }; return (
setIsOpen(!isOpen)} className={clsx('cursor-pointer', triggerClassName)} role="button" aria-haspopup="true" aria-expanded={isOpen} > {trigger}
{isOpen && ( <>
setIsOpen(false)} aria-hidden="true" />
{items.map((item, index) => { if (item.divider) { return (
); } return ( ); })}
)}
); }; /** * Simple select dropdown (alternative to native select) */ export interface SelectDropdownProps { value: string; onChange: (value: string) => void; options: Array<{ label: string; value: string; disabled?: boolean }>; placeholder?: string; className?: string; disabled?: boolean; } export const SelectDropdown = ({ value, onChange, options, placeholder = 'Select...', className, disabled, }: SelectDropdownProps) => { const selectedOption = options.find((opt) => opt.value === value); return ( {selectedOption?.label || placeholder}
} items={options.map((opt) => ({ label: opt.label, value: opt.value, disabled: opt.disabled, onClick: () => onChange(opt.value), }))} className="w-full" triggerClassName="w-full" /> ); };