import type { ProvenanceDTO } from "@dci/core"; import { ExternalLink } from "lucide-react"; import type { ReactNode } from "react"; import { ConfidenceBadge, EstimateMark, ScopeBadge } from "@/components/ui/badge"; import { SourceBadge } from "@/components/ui/source-badge"; import { cn } from "@/components/ui/cn"; import { ProvenancePopover } from "@/components/ui/popover"; import { EmptyState } from "@/components/ui/states"; import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table"; import { fieldLabel, valueHint } from "@/lib/field-labels"; import { fmtDate, fmtValue } from "@/lib/format"; /** Provenance rows backing one or more fields (API field names are camelCase; snake_case tolerated). */ export function provFor(items: ProvenanceDTO[] | undefined, ...fields: string[]): ProvenanceDTO[] { if (!items?.length) return []; const want = new Set(fields.flatMap((f) => [f, f.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`), f.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())])); return items.filter((p) => want.has(p.field)); } export { FIELD_LABEL, fieldLabel } from "@/lib/field-labels"; /** Value + inline provenance popover for the rows backing `fields`. Renders nothing extra when no provenance exists. */ export function WithProvenance({ items, fields, children, className }: { items: ProvenanceDTO[] | undefined; fields: string | string[]; children: ReactNode; className?: string }) { const rows = provFor(items, ...(Array.isArray(fields) ? fields : [fields])); return ( {children} {rows.length > 0 && } ); } /** Full provenance table: field, value, source, first/last observed, confidence, method, link. Stacks to cards < 640 px. */ export function ProvenanceTable({ items, emptyDescription = "No field-level provenance has been published for this record yet." }: { items: ProvenanceDTO[]; emptyDescription?: string }) { if (!items.length) return ; const sorted = [...items].sort((a, b) => fieldLabel(a.field).localeCompare(fieldLabel(b.field)) || (a.lastObserved < b.lastObserved ? 1 : -1)); return ( {sorted.map((p, i) => ( ))}
Field Value Source First observed Last observed Confidence Method Link
{fieldLabel(p.field)} {p.isWinner && ( shown )} {p.isEstimate && } {fmtValue(p.value, valueHint(p.field))} {p.scope && } {p.sourceName} {fmtDate(p.firstObserved)} {fmtDate(p.lastObserved)} {p.method ?? "—"} source
); }