import { CodeIcon } from "@chopin/icons"; import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, } from "react"; import { createPortal } from "react-dom"; import { evidencePoint } from "../evidence-geometry"; import { planScroller } from "../scroll"; import type { ReactNode } from "react"; type Position = { top: number; left: number; side: "right" | "left" }; type EvidenceControl = { active: boolean; open: boolean; id: string; trigger: React.RefObject; toggle: () => void; }; let EvidenceContext = createContext(null); export function EvidenceTrigger() { let evidence = useContext(EvidenceContext); if (!evidence?.active) return null; return ( ); } /** Evidence remains card-local, but opens only through its header control. */ export function EvidenceHover({ active, children, content, question }: { active: boolean; children: ReactNode; content: ReactNode | null; question: string; }) { let [open, setOpen] = useState(false); let [position, setPosition] = useState(); let card = useRef(null); let panel = useRef(null); let trigger = useRef(null); let focusPending = useRef(false); let id = useId(); let visible = active && !!content && open; let dismiss = useCallback((returnFocus = false) => { focusPending.current = false; setOpen(false); setPosition(undefined); if (returnFocus) trigger.current?.focus({ preventScroll: true }); }, []); useEffect(() => { if (!active || !content) dismiss(); }, [active, content, dismiss]); useLayoutEffect(() => { if (!visible || !position || !focusPending.current) return; focusPending.current = false; panel.current?.querySelector("button:not(:disabled)")?.focus(); }, [visible, position]); useEffect(() => { if (!visible) return; let onKey = (event: KeyboardEvent) => { if (event.key !== "Escape") return; event.preventDefault(); event.stopPropagation(); dismiss(true); }; let onPointerDown = (event: PointerEvent) => { let target = event.target; if (!(target instanceof Node)) return; if (!trigger.current?.contains(target) && !panel.current?.contains(target)) dismiss(); }; let onFocus = (event: FocusEvent) => { let target = event.target; if (!(target instanceof Node)) return; if (!trigger.current?.contains(target) && !panel.current?.contains(target)) dismiss(); }; let onHidden = () => { if (document.hidden) dismiss(); }; document.addEventListener("keydown", onKey, true); document.addEventListener("pointerdown", onPointerDown, true); document.addEventListener("focusin", onFocus); document.addEventListener("visibilitychange", onHidden); return () => { document.removeEventListener("keydown", onKey, true); document.removeEventListener("pointerdown", onPointerDown, true); document.removeEventListener("focusin", onFocus); document.removeEventListener("visibilitychange", onHidden); }; }, [visible, dismiss]); useLayoutEffect(() => { if (!visible) return; let anchor = card.current?.querySelector("article"); let surface = panel.current; if (!anchor || !surface) return; let place = () => { if (!anchor.isConnected || anchor.getClientRects().length === 0) { dismiss(); return; } let bounds = anchor.getBoundingClientRect(); let scroller = planScroller(card.current); let viewport = scroller?.getBoundingClientRect(); if ( viewport && (bounds.bottom <= viewport.top || bounds.top >= viewport.bottom) ) { dismiss(); return; } let next = evidencePoint( bounds, { width: innerWidth, height: innerHeight }, surface.offsetWidth, surface.offsetHeight, ); setPosition(current => current?.left === next.left && current.top === next.top && current.side === next.side ? current : next ); }; place(); let observer = new ResizeObserver(place); observer.observe(anchor); observer.observe(surface); window.addEventListener("resize", place); document.addEventListener("scroll", place, true); return () => { observer.disconnect(); window.removeEventListener("resize", place); document.removeEventListener("scroll", place, true); }; }, [visible, content, dismiss]); return ( { if (visible) dismiss(); else { focusPending.current = true; setOpen(true); } }, }} >
{children} {visible && createPortal(
{ if (event.target instanceof Element && event.target.closest("button")) dismiss(); }} ref={panel} role="dialog" style={position ? { left: position.left, top: position.top } : { left: 0, top: 0, visibility: "hidden" }} >
{content}
, document.body, )}
); }