"use client"; import * as React from "react"; import { MoreVertical } from "lucide-react"; import { cn } from "@/lib/utils"; export type MenuItem = { label: string; onClick: () => void; destructive?: boolean; }; // Menu geometry, for keeping it on screen: w-44, rows of py-2 text-sm. const MENU_WIDTH = 176; const MENU_ROW = 36; const MENU_GAP = 8; /** * A kebab (⋯) trigger with a small dropdown of actions — keeps secondary / * destructive row actions hidden until opened. The menu is fixed-positioned * from the trigger so the table's overflow container can't clip it; it closes * on outside-click, Escape, or scroll. */ export function RowMenu({ items }: { items: MenuItem[] }) { const [open, setOpen] = React.useState(false); const [pos, setPos] = React.useState<{ top: number; right: number } | null>( null, ); const btnRef = React.useRef(null); const menuRef = React.useRef(null); React.useEffect(() => { if (!open) return; function onDown(e: MouseEvent) { const t = e.target as Node; if (btnRef.current?.contains(t) || menuRef.current?.contains(t)) return; setOpen(false); } function onKey(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } function onScroll() { setOpen(false); } document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey); window.addEventListener("scroll", onScroll, true); return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); window.removeEventListener("scroll", onScroll, true); }; }, [open]); function toggle() { if (!open && btnRef.current) { const r = btnRef.current.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; // Right-aligned to the button, but kept on screen: on a phone the // button can sit at the left of a card, where a right-aligned menu // would hang off the edge. Near the bottom it opens upwards instead. const right = Math.max( MENU_GAP, Math.min(vw - r.right, vw - MENU_WIDTH - MENU_GAP), ); const height = items.length * MENU_ROW + 8; const top = r.bottom + 4 + height > vh - MENU_GAP ? Math.max(MENU_GAP, r.top - 4 - height) : r.bottom + 4; setPos({ top, right }); } setOpen((v) => !v); } return ( <> {open && pos ? (
{items.map((it, i) => ( ))}
) : null} ); }