import { useState, type ReactNode } from "react"; import { MoreHorizontal } from "lucide-react"; import { PopoverItem, PopoverSurface, useAnchoredPopover } from "./AnchoredPopover"; // A reusable "⋯ more actions" overflow menu: a compact trigger that opens a list of one-shot actions. // Built on the R8 AnchoredPopover platform (arrows/typeahead/Escape/anchor-flip), same as ViewSwitcher // and LanguageSwitcher. Use it to keep occasional actions out of a crowded toolbar. The items are // fire-and-close (`onSelect`); a `disabled` item (e.g. a mutation already in flight) is inert. export interface ActionMenuItem { id: string; label: string; icon?: ReactNode; // an element (so a caller can swap in a spinning icon while pending) onSelect: () => void; disabled?: boolean; title?: string; // optional native tooltip (carries a longer hint the label alone can't) } export default function ActionMenu({ items, ariaLabel, className, trigger, }: { items: readonly ActionMenuItem[]; /** Accessible name for the trigger (e.g. "Catalog actions"). */ ariaLabel: string; /** Trigger className override; omit for the default glass ⋯ icon button. */ className?: string; /** Custom trigger content; omit for the default ⋯ icon. */ trigger?: ReactNode; }) { const [open, setOpen] = useState(false); // A disabled item (a mutation already in flight) is skipped by roving keyboard nav automatically: // PopoverItem renders it as a native `disabled` button, which useListNavigation reads and never // focuses. No explicit disabledIndices needed. See ActionMenu.dom.test.tsx for the guard. const pop = useAnchoredPopover({ open, onOpenChange: setOpen, placement: "bottom-end" }); return ( <> {items.map((it) => ( { it.onSelect(); setOpen(false); }} className="flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left text-sm outline-hidden transition disabled:opacity-50" > {it.icon && {it.icon}} {it.label} ))} ); }