import React, { useEffect } from 'react'; export interface ModalProps { open: boolean; onClose: () => void; title?: string; children: React.ReactNode; width?: number | string; /** Override the backdrop's own inline styles (e.g. a different overlay * opacity/z-index). Merged on top of the shared defaults. */ backdropStyle?: React.CSSProperties; /** Escape key closes the modal. Default true — set false to preserve a * modal that never handled Escape before this component existed. */ closeOnEscape?: boolean; /** Clicking the backdrop (outside the modal content) closes the modal. * Default true — set false to preserve a modal that never handled * backdrop clicks before this component existed. */ closeOnBackdropClick?: boolean; /** Extra attributes spread onto the backdrop div (e.g. role="dialog", * aria-modal) for a modal that previously set those directly. */ backdropProps?: React.HTMLAttributes; } const defaultBackdropStyle: React.CSSProperties = { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.65)', display: 'flex', alignItems: 'center', justifyContent: 'center', /* z-index comes from the `.modal-backdrop` class (--z-modal in the shared layer scale, editor.css) rather than an inline value, so every Modal consumer -- including ones that pass a custom `backdropStyle` without a zIndex, like SitesmithModal -- lands on the same layer. */ }; /* ---------- Shared modal chrome ---------- Backdrop + centered panel wrapper + Escape-to-close + scroll-lock, extracted from TemplateModal / HeadCodeModal / SitesmithModal (task E4.3). Each modal's own content — including its own header, close button, and panel styling (background, radius, shadow, dimensions) — stays in that modal's own file and is passed in as `children`, since those visuals differ across the three. `closeOnEscape` / `closeOnBackdropClick` default to true (the behavior TemplateModal and HeadCodeModal already had); SitesmithModal had neither before this extraction, so it opts out of both to keep its exact prior behavior. */ export const Modal: React.FC = ({ open, onClose, title, children, width, backdropStyle, closeOnEscape = true, closeOnBackdropClick = true, backdropProps, }) => { useEffect(() => { if (!open || !closeOnEscape) return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [open, closeOnEscape, onClose]); useEffect(() => { if (!open) return; const prevOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prevOverflow; }; }, [open]); if (!open) return null; return (
{ if (closeOnBackdropClick && e.target === e.currentTarget) onClose(); }} >
{title !== undefined && (
{title}
)} {children}
); };