68 lines
1.8 KiB
TypeScript
68 lines
1.8 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useCallback, useEffect, useRef } from "react";
|
||
|
|
import { Pencil, Copy, Trash2, Scissors } from "lucide-react";
|
||
|
|
|
||
|
|
interface ContextMenuItem {
|
||
|
|
label: string;
|
||
|
|
icon: React.ReactNode;
|
||
|
|
onClick: () => void;
|
||
|
|
danger?: boolean;
|
||
|
|
disabled?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface NodeContextMenuProps {
|
||
|
|
x: number;
|
||
|
|
y: number;
|
||
|
|
items: ContextMenuItem[];
|
||
|
|
onClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function NodeContextMenu({ x, y, items, onClose }: NodeContextMenuProps) {
|
||
|
|
const ref = useRef<HTMLDivElement>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const handleClick = (e: MouseEvent) => {
|
||
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
const handleEsc = (e: KeyboardEvent) => {
|
||
|
|
if (e.key === "Escape") onClose();
|
||
|
|
};
|
||
|
|
document.addEventListener("mousedown", handleClick);
|
||
|
|
document.addEventListener("keydown", handleEsc);
|
||
|
|
return () => {
|
||
|
|
document.removeEventListener("mousedown", handleClick);
|
||
|
|
document.removeEventListener("keydown", handleEsc);
|
||
|
|
};
|
||
|
|
}, [onClose]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
ref={ref}
|
||
|
|
className="fixed z-[200] min-w-[160px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl shadow-black/40"
|
||
|
|
style={{ left: x, top: y }}
|
||
|
|
>
|
||
|
|
{items.map((item, i) => (
|
||
|
|
<button
|
||
|
|
key={i}
|
||
|
|
onClick={() => {
|
||
|
|
item.onClick();
|
||
|
|
onClose();
|
||
|
|
}}
|
||
|
|
disabled={item.disabled}
|
||
|
|
className={`flex w-full items-center gap-2.5 px-3 py-2 text-left text-xs transition-colors disabled:opacity-30 ${
|
||
|
|
item.danger
|
||
|
|
? "text-pink-400 hover:bg-pink-500/10"
|
||
|
|
: "text-zinc-300 hover:bg-zinc-800"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{item.icon}
|
||
|
|
{item.label}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|