import { CircleAlert } from "lucide-react"; import type { ReactNode } from "react"; import { displayUsername, formatDate } from "../lib/format"; import type { AccountUsername, AuditLogRow } from "../types"; type Tone = "neutral" | "good" | "danger" | "warn"; export function PageFrame({ title, eyebrow, children, actions }: { title: string; eyebrow?: string; children: ReactNode; actions?: ReactNode; }) { return (
{eyebrow &&
{eyebrow}
}

{title}

{actions &&
{actions}
}
{children}
); } export function QueryPanel({ children }: { children: ReactNode }) { return
{children}
; } export function SplitLayout({ main, side }: { main: ReactNode; side: ReactNode }) { return (
{main}
); } export function SectionHead({ title, text, action }: { title: string; text?: string; action?: ReactNode }) { return (

{title}

{text &&

{text}

}
{action &&
{action}
}
); } export function Alert({ children }: { children: ReactNode }) { return
{children}
; } export function Badge({ children, tone = "neutral" }: { children: ReactNode; tone?: Tone }) { return {children}; } export function StatusItem({ label, value, tone }: { label: string; value: string; tone: "neutral" | "good" | "warn" }) { return (
{label} {value}
); } export function Metric({ label, value, tone = "neutral", mono = false, loading = false }: { label: string; value: string; tone?: Tone; mono?: boolean; loading?: boolean; }) { return (
{label} {loading ? : value}
); } export function Summary({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { return (
{label} {value}
); } export function AuditTable({ rows }: { rows: AuditLogRow[] }) { return (
{rows.map((row) => ( ))} {rows.length === 0 && }
{"ID"}{"Command ID"}{"Action"}{"Actor"}{"Status"}{"Dry-run"}{"Reason"}{"Time"}
{row.ID} {row.CommandID} {row.Action} {row.Actor} {row.Status} {row.DryRun ? "Yes" : "No"} {row.Reason} {formatDate(row.CreatedAt)}
); } export function EmptyRow({ colSpan }: { colSpan: number }) { return {"No results"}; } // Placeholder rows for a table that has nothing yet *because it is still // loading* -- distinct from EmptyRow, which asserts the query genuinely // returned nothing. Showing "No results" during the first fetch reads as a // wrong answer rather than a pending one. export function LoadingRow({ colSpan, rows = 3 }: { colSpan: number; rows?: number }) { return ( <> {Array.from({ length: rows }, (_, row) => ( {Array.from({ length: colSpan }, (_unused, cell) => ( ))} ))} ); } export function LoadingSurface({ label }: { label: string }) { return
{label}
; } export function JsonBlock({ value }: { value: string }) { return
{value || "{}"}
; } // UsernameCell renders a peer's editable username with its collectible usernames // branching off underneath, in the order clients project them. // // An inactive collectible is shown rather than hidden: the peer still owns it, it // just does not resolve publicly, and an operator looking for "where did that name // go" needs to see it. It is marked instead of dropped. // Pass an empty username to render the branch on its own, which is what the // detail header does: it already shows the editable slot on the line above. export function UsernameCell({ username, collectibles }: { username?: string; collectibles?: AccountUsername[] | null }) { const main = displayUsername(username ?? ""); const branch = collectibles ?? []; if (branch.length === 0) { return <>{main || "-"}; } return ( <> {main} ); }