| import { useEffect } from "react"; | |
| import { createPortal } from "react-dom"; | |
| import "./Modal.css"; | |
| /** | |
| * Shared foundation for both centered dialogs and side drawers. | |
| * variant: "dialog" | "drawer" | |
| */ | |
| export function Modal({ | |
| open, | |
| onClose, | |
| title, | |
| children, | |
| footer, | |
| variant = "dialog", | |
| width = 480, | |
| }) { | |
| useEffect(() => { | |
| if (!open) return; | |
| const onKeyDown = (e) => { | |
| if (e.key === "Escape") onClose?.(); | |
| }; | |
| window.addEventListener("keydown", onKeyDown); | |
| return () => window.removeEventListener("keydown", onKeyDown); | |
| }, [open, onClose]); | |
| if (!open) return null; | |
| return createPortal( | |
| <div className="dw-modal-backdrop" onMouseDown={onClose}> | |
| <div | |
| className={`dw-modal dw-modal--${variant}`} | |
| style={variant === "dialog" ? { width } : undefined} | |
| onMouseDown={(e) => e.stopPropagation()} | |
| role="dialog" | |
| aria-modal="true" | |
| > | |
| {title && ( | |
| <div className="dw-modal__header"> | |
| <h3>{title}</h3> | |
| <button className="dw-modal__close" onClick={onClose} aria-label="Close"> | |
| × | |
| </button> | |
| </div> | |
| )} | |
| <div className="dw-modal__body">{children}</div> | |
| {footer && <div className="dw-modal__footer">{footer}</div>} | |
| </div> | |
| </div>, | |
| document.body | |
| ); | |
| } | |
| /** Convenience wrapper — same foundation, right-side drawer defaults. */ | |
| export function Drawer(props) { | |
| return <Modal variant="drawer" {...props} />; | |
| } | |