Download app/frontend/src/components/common.tsx from ml-intern-explorers/primarydraft: direct link, hf CLI and curl.
- Browser
- Download file 4.81 kB
-
https://huggingface.co/spaces/ml-intern-explorers/primarydraft/resolve/main/app/frontend/src/components/common.tsx
- Command line
-
hf download hf://spaces/ml-intern-explorers/primarydraft/app/frontend/src/components/common.tsx
-
curl -L -o common.tsx https://huggingface.co/spaces/ml-intern-explorers/primarydraft/resolve/main/app/frontend/src/components/common.tsx
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 }; | |