import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; // Flyout generico con light-dismiss: click fuera o Esc cierra. trigger recibe { onClick, ref }. // Posicion en dos fases (igual tecnica que ComboBox): se abre debajo/alineado al trigger, se mide // el panel ya renderizado y, si se sale del viewport, se voltea hacia arriba / se recorta hacia // adentro -- nunca se deja que el menu quede cortado por el borde de la ventana (ver // project-md-templates/leeme_primero.txt, seccion UI/UX: "menus flotantes siempre dentro del // viewport"). export function Flyout({ trigger, children, align = 'end' }) { const [open, setOpen] = useState(false); const [pos, setPos] = useState(null); // { top, left, anchorTop, anchorBottom, anchorLeft, anchorRight, ready } const anchorRef = useRef(null); const panelRef = useRef(null); function openFlyout() { const rect = anchorRef.current.getBoundingClientRect(); setPos({ top: rect.bottom + 6, left: align === 'end' ? rect.right : rect.left, anchorTop: rect.top, anchorBottom: rect.bottom, anchorLeft: rect.left, anchorRight: rect.right, ready: false, }); setOpen(true); } // Fase 2: ya con el panel en el DOM (oculto), se mide su tamaƱo real y se ajusta top/left para // que quepa entero en el viewport antes de mostrarlo -- sin esto, un trigger cerca del borde // inferior/derecho deja el menu cortado por el borde de la ventana. useLayoutEffect(() => { if (!open || !pos || pos.ready || !panelRef.current) return; const panelRect = panelRef.current.getBoundingClientRect(); const margin = 8; let top = pos.top; if (pos.anchorBottom + 6 + panelRect.height > window.innerHeight - margin) { const openUpTop = pos.anchorTop - 6 - panelRect.height; top = openUpTop >= margin ? openUpTop : Math.max(margin, window.innerHeight - margin - panelRect.height); } let left = align === 'end' ? pos.anchorRight - panelRect.width : pos.anchorLeft; if (left + panelRect.width > window.innerWidth - margin) left = window.innerWidth - margin - panelRect.width; if (left < margin) left = margin; setPos((p) => ({ ...p, top, left, ready: true })); }, [open, pos]); useEffect(() => { if (!open) return; function onKey(e) { if (e.key === 'Escape') setOpen(false); } document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [open]); return ( <> {trigger({ onClick: () => (open ? setOpen(false) : openFlyout()) })} {open && pos && createPortal( <>