import { useEffect, useRef } from 'react'; import { createRoot, type Root } from 'react-dom/client'; interface ModalProps { isOpen: boolean; onClose: () => void; children: React.ReactNode; title?: string; } export function Modal({ isOpen, onClose, children, title }: ModalProps) { const overlayRef = useRef(null); const bodyRef = useRef(null); const rootRef = useRef(null); const onCloseRef = useRef(onClose); onCloseRef.current = onClose; // Create/destroy modal DOM shell using vanilla DOM (HTML namespace guaranteed) useEffect(() => { if (!isOpen) return; // Overlay const overlay = document.createElement('div'); overlay.className = 'aisthetix-modal-overlay'; overlay.addEventListener('click', () => onCloseRef.current()); // Content container (stops propagation) const content = document.createElement('div'); content.className = 'aisthetix-modal-content'; content.setAttribute('role', 'dialog'); content.setAttribute('aria-modal', 'true'); content.addEventListener('click', (e) => e.stopPropagation()); // Header const header = document.createElement('div'); header.className = 'aisthetix-modal-header'; if (title) { const h2 = document.createElement('h2'); h2.id = 'modal-title'; h2.className = 'aisthetix-modal-title'; h2.textContent = title; content.setAttribute('aria-labelledby', 'modal-title'); header.appendChild(h2); } // Close button const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 'aisthetix-modal-close'; closeBtn.setAttribute('aria-label', 'Close'); closeBtn.innerHTML = '' + '' + ''; closeBtn.addEventListener('click', () => onCloseRef.current()); header.appendChild(closeBtn); // Body (React renders children here) const body = document.createElement('div'); body.className = 'aisthetix-modal-body'; content.appendChild(header); content.appendChild(body); overlay.appendChild(content); document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; overlayRef.current = overlay; bodyRef.current = body; rootRef.current = createRoot(body); return () => { const root = rootRef.current; const el = overlayRef.current; rootRef.current = null; overlayRef.current = null; bodyRef.current = null; document.body.style.overflow = ''; if (root) { setTimeout(() => { root.unmount(); el?.remove(); }, 0); } }; }, [isOpen]); // Update title when it changes useEffect(() => { if (!overlayRef.current) return; const h2 = overlayRef.current.querySelector('#modal-title'); if (h2 && title) h2.textContent = title; }, [title]); // Render React children into the body div useEffect(() => { if (rootRef.current && isOpen) { rootRef.current.render(<>{children}); } }); // Close on Escape key useEffect(() => { if (!isOpen) return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [isOpen, onClose]); return null; }