import { useEffect, useState } from "react"; import { WarningCircle, CircleNotch, FileText, Info, } from "@phosphor-icons/react"; import type { Fact, Provenance } from "../lib/api"; export const ICON = { size: 16, weight: "regular" as const }; /** Error state. Inline, specific, never a bare "something went wrong". */ export function ErrorNote({ message, onRetry }: { message: string; onRetry?: () => void }) { return (
{message}
{onRetry && ( )}
); } /** Loading state shaped like the table it replaces, not a spinner in a void. */ export function TableSkeleton({ rows = 8 }: { rows?: number }) { return (
{Array.from({ length: rows }).map((_, i) => (
))}
); } export function Empty({ icon, title, children, }: { icon?: React.ReactNode; title: string; children?: React.ReactNode }) { return (
{icon ?? }

{title}

{children &&

{children}

}
); } export function Spinner() { return ; } export function Note({ tone = "accent", children, }: { tone?: "accent" | "warn" | "quiet"; children: React.ReactNode }) { const cls = tone === "warn" ? "note note-warn" : tone === "quiet" ? "note note-quiet" : "note"; return
{children}
; } /** Citation popover. Follows the pointer, driven by native hover events. */ export function CitationPopover({ container }: { container: React.RefObject }) { const [pop, setPop] = useState<{ x: number; y: number; source: string; locator: string; raw: string; status: string; } | null>(null); useEffect(() => { // Listen on the document, not on container.current. The container renders // only once its content has loaded, so an effect that reads the ref on mount // finds null and silently never attaches: the popover would never appear. const onOver = (e: Event) => { const t = (e.target as HTMLElement)?.closest?.("cite"); if (!t) return; if (container.current && !container.current.contains(t)) return; const r = t.getBoundingClientRect(); setPop({ x: Math.min(r.left, window.innerWidth - 440), y: r.bottom + 8, source: t.getAttribute("data-source") ?? "", locator: t.getAttribute("data-locator") ?? "", raw: t.getAttribute("data-raw") ?? "", status: t.getAttribute("data-status") ?? "", }); }; const onOut = (e: Event) => { if ((e.target as HTMLElement)?.closest?.("cite")) setPop(null); }; document.addEventListener("mouseover", onOver); document.addEventListener("mouseout", onOut); return () => { document.removeEventListener("mouseover", onOver); document.removeEventListener("mouseout", onOut); }; }, [container]); if (!pop) return null; return (
{pop.status === "proposed" ? "proposed issue term" : `sourced ยท ${pop.status}`}
{pop.source}
{pop.locator}
{pop.raw &&
{pop.raw}
}
); } /** Provenance rendered as a list, used in the ledger drawer. */ export function ProvenanceList({ provenance }: { provenance: Provenance[] }) { return (
{provenance.map((p, i) => (
{p.source_title}
{p.locator}
{p.raw_line &&
{p.raw_line}
}
))}
); } export function StatusChip({ status }: { status: string }) { return {status}; } export function factTone(f: Fact): string { return f.status; } export { Info };