Bot-Derpy's picture
PrimaryDraft: SEBI TechSprint prototype (27 deterministic checks, 40 real BSE SME issuers)
baf8a7b verified
Raw History Blame Contribute Delete
4.81 kB
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 (
<div className="err" role="alert">
<WarningCircle size={17} weight="fill" style={{ flexShrink: 0, marginTop: 1 }} />
<div>
<div>{message}</div>
{onRetry && (
<button className="btn btn-ghost btn-sm" style={{ marginTop: 8 }} onClick={onRetry}>
Try again
</button>
)}
</div>
</div>
);
}
/** Loading state shaped like the table it replaces, not a spinner in a void. */
export function TableSkeleton({ rows = 8 }: { rows?: number }) {
return (
<div aria-busy="true" aria-label="Loading">
{Array.from({ length: rows }).map((_, i) => (
<div className="skel-row" key={i}>
<div className="skel" style={{ width: `${58 + ((i * 13) % 34)}%` }} />
<div className="skel" style={{ width: "68%" }} />
<div className="skel" style={{ width: `${44 + ((i * 21) % 42)}%` }} />
<div className="skel" style={{ width: "56%" }} />
</div>
))}
</div>
);
}
export function Empty({
icon, title, children,
}: { icon?: React.ReactNode; title: string; children?: React.ReactNode }) {
return (
<div className="empty">
<div className="ico">{icon ?? <FileText size={26} />}</div>
<h3>{title}</h3>
{children && <p>{children}</p>}
</div>
);
}
export function Spinner() {
return <CircleNotch size={15} className="spin" aria-hidden />;
}
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 <div className={cls}>{children}</div>;
}
/** Citation popover. Follows the pointer, driven by native hover events. */
export function CitationPopover({ container }: { container: React.RefObject<HTMLElement | null> }) {
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 (
<div className="pop" style={{ left: pop.x, top: pop.y }} role="tooltip">
<div className="pop-t">
{pop.status === "proposed" ? "proposed issue term" : `sourced · ${pop.status}`}
</div>
<div style={{ fontWeight: 500 }}>{pop.source}</div>
<div style={{ color: "#93a5b8", marginTop: 2 }}>{pop.locator}</div>
{pop.raw && <div className="pop-raw">{pop.raw}</div>}
</div>
);
}
/** Provenance rendered as a list, used in the ledger drawer. */
export function ProvenanceList({ provenance }: { provenance: Provenance[] }) {
return (
<div style={{ display: "grid", gap: 6 }}>
{provenance.map((p, i) => (
<div key={i} style={{ fontSize: 11.5, lineHeight: 1.5 }}>
<div style={{ fontWeight: 500 }}>{p.source_title}</div>
<div className="f-src">{p.locator}</div>
{p.raw_line && <div className="ev-raw">{p.raw_line}</div>}
</div>
))}
</div>
);
}
export function StatusChip({ status }: { status: string }) {
return <span className={`chip chip-${status}`}>{status}</span>;
}
export function factTone(f: Fact): string {
return f.status;
}
export { Info };