// components/ui/Dropdown.tsx 'use client'; import * as React from 'react'; import { Menu, MenuButton, MenuItem, MenuItems, Portal, } from '@headlessui/react'; import { ChevronDownIcon, EllipsisVerticalIcon, } from '@heroicons/react/20/solid'; import clsx from 'clsx'; type DropdownTone = 'default' | 'danger'; export type DropdownItem = { id?: string; label: string; href?: string; onClick?: () => void; icon?: React.ReactNode; tone?: DropdownTone; disabled?: boolean; }; export type DropdownSection = { id?: string; label?: string; items: DropdownItem[]; }; export type DropdownTriggerVariant = 'button' | 'icon'; export interface DropdownProps { label?: string; ariaLabel?: string; align?: 'left' | 'right'; triggerVariant?: DropdownTriggerVariant; header?: React.ReactNode; sections: DropdownSection[]; triggerClassName?: string; menuClassName?: string; /** Dropdown komplett deaktivieren (Trigger klickt nicht) */ disabled?: boolean; } /* ───────── interne Helfer ───────── */ const itemBaseClasses = 'block px-4 py-2 text-sm ' + // Default Text 'text-gray-700 dark:text-gray-300 ' + // Hover (Maus) 'hover:bg-gray-100 hover:text-gray-900 ' + 'dark:hover:bg-white/5 dark:hover:text-white ' + // Focus Outline weglassen 'focus:outline-none'; const itemWithIconClasses = 'group flex items-center gap-x-3 px-4 py-2 text-sm ' + // Default Text 'text-gray-700 dark:text-gray-300 ' + // Hover 'hover:bg-gray-100 hover:text-gray-900 ' + 'dark:hover:bg-white/5 dark:hover:text-white ' + 'focus:outline-none'; const iconClasses = 'flex size-5 shrink-0 items-center justify-center text-gray-400 ' + 'group-hover:text-gray-500 ' + 'dark:text-gray-500 dark:group-hover:text-white'; const toneClasses: Record = { default: '', danger: 'text-rose-600 data-focus:text-rose-700 dark:text-rose-400 dark:data-focus:text-rose-300', }; function renderItemContent(item: DropdownItem) { const hasIcon = !!item.icon; if (hasIcon) { return ( {item.label} ); } return ( {item.label} ); } /* ───────── Dropdown-Komponente (mit Portal) ───────── */ export function Dropdown({ label = 'Options', ariaLabel = 'Open options', align = 'right', triggerVariant = 'button', header, sections, triggerClassName, menuClassName, disabled = false, }: DropdownProps) { const hasDividers = sections.length > 1; const triggerIsButton = triggerVariant === 'button'; const buttonRef = React.useRef(null); const [position, setPosition] = React.useState<{ top: number; left: number; } | null>(null); return ( {({ open }) => { React.useEffect(() => { if (!open || !buttonRef.current) return; const rect = buttonRef.current.getBoundingClientRect(); const scrollX = window.pageXOffset || document.documentElement.scrollLeft; const scrollY = window.pageYOffset || document.documentElement.scrollTop; const left = align === 'left' ? rect.left + scrollX : rect.right + scrollX; setPosition({ top: rect.bottom + scrollY + 4, left, }); }, [open, align]); return ( <> {triggerIsButton ? ( <> {label} {open && position && !disabled && ( {header &&
{header}
} {sections.map((section, sectionIndex) => (
{/* NEU: Gruppen-Label als "Trenner" */} {section.label && (
{section.label}
)} {section.items.map((item, itemIndex) => { const key = item.id ?? `${sectionIndex}-${itemIndex}`; return ( {item.href ? ( {renderItemContent(item)} ) : ( )} ); })}
))}
)} ); }}
); } export default Dropdown;