import React, { useEffect, useCallback, useRef } from 'react' import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { lockBodyScroll } from '../../utils/bodyScrollLock' const sizeClasses: Record = { sm: 'max-w-sm', md: 'max-w-md', lg: 'max-w-lg', xl: 'max-w-2xl', '2xl': 'max-w-4xl', '3xl': 'max-w-5xl', // Wide enough for the add-place dialog to carry a detail column beside the // form, and both together beside the collection picker. '4xl': 'max-w-6xl', '5xl': 'max-w-7xl', } interface ModalProps { isOpen: boolean onClose: () => void title?: React.ReactNode children?: React.ReactNode size?: string footer?: React.ReactNode hideCloseButton?: boolean } export default function Modal({ isOpen, onClose, title, children, size = 'md', footer, hideCloseButton = false, }: ModalProps) { const handleEsc = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }, [onClose]) useEffect(() => { if (!isOpen) return document.addEventListener('keydown', handleEsc) return () => document.removeEventListener('keydown', handleEsc) }, [isOpen, handleEsc]) // Separate from the key listener so a new onClose identity does not release // and re-take the lock on every render. The shared lock is ref-counted: this // modal must not clear a lock another overlay is still holding (#1809). useEffect(() => { if (!isOpen) return return lockBodyScroll() }, [isOpen]) const mouseDownTarget = useRef(null) if (!isOpen) return null return createPortal(
{ mouseDownTarget.current = e.target }} onClick={e => { if (e.target === e.currentTarget && mouseDownTarget.current === e.currentTarget) onClose() mouseDownTarget.current = null }} >
e.stopPropagation()} > {/* Header — stays put even while the body scrolls */}

{title}

{!hideCloseButton && ( )}
{/* Body — scrolls when content overflows. min-h-0 lets the flex child shrink below its intrinsic height. */}
{children}
{/* Footer — sticky at the bottom of the modal, never compressed */} {footer && (
{footer}
)}
, document.body ) }