import { useEffect, useRef, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { X } from "lucide-react"; import { useBackToClose } from "../lib/history"; // Stack of open modals so ESC only closes the topmost one — e.g. an error dialog over the // tag editor: pressing ESC dismisses just the error and returns to the editor underneath. let modalStack: number[] = []; let nextModalId = 1; // Small centered modal shell (portaled to ): backdrop + ESC + scroll-lock close. export default function Modal({ title, onClose, children, maxWidth = "max-w-lg", }: { title: ReactNode; onClose: () => void; children: ReactNode; maxWidth?: string; }) { // Browser/mouse Back closes the topmost modal instead of navigating away. useBackToClose(onClose); // Backdrop-click close must only fire when the press STARTED on the backdrop too. Otherwise a // drag that begins inside the dialog (e.g. selecting text in an input) and is released out on // the backdrop wrongly dismisses — the click event bubbles to the common ancestor (the backdrop). const pressedOnBackdrop = useRef(false); // Keep a stable handler across renders so the stack id is assigned once per mount. const onCloseRef = useRef(onClose); onCloseRef.current = onClose; useEffect(() => { const id = nextModalId++; modalStack.push(id); const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && modalStack[modalStack.length - 1] === id) { e.stopPropagation(); onCloseRef.current(); } }; window.addEventListener("keydown", onKey); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); modalStack = modalStack.filter((x) => x !== id); document.body.style.overflow = prev; }; }, []); return createPortal(
{ pressedOnBackdrop.current = e.target === e.currentTarget; }} onClick={(e) => { if (pressedOnBackdrop.current && e.target === e.currentTarget) onClose(); }} role="dialog" aria-modal="true" >

{title}

{children}
, document.body ); }