import Link from 'next/link'; import { ExternalLink } from 'lucide-react'; import { fmtDate } from '@/lib/format'; export interface ProvenanceInfo { sourceSlug: string; sourceName?: string | null; dataset?: string | null; datasetVersion?: string | null; retrievedAt?: Date | string | null; sourceUrl?: string | null; layer?: 'raw' | 'normalized' | 'canonical' | 'derived' | 'ranked'; license?: string | null; evidenceType?: string | null; pmid?: string | null; ingestRunId?: string | null; note?: string | null; } /** One-line provenance summary for `title` attributes on compact badges (dataset · version · retrieved). */ export function provenanceTitle(p: ProvenanceInfo): string { const bits = [p.sourceName ?? p.sourceSlug]; if (p.dataset) bits.push(p.dataset); if (p.datasetVersion) bits.push(`version ${p.datasetVersion}`); if (p.retrievedAt) bits.push(`retrieved ${fmtDate(p.retrievedAt)}`); if (p.layer === 'derived' || p.layer === 'ranked') bits.push('computed by CancerIndex'); return bits.join(' · '); } /** * Small source badge; hover / focus reveals the provenance popover (source, dataset, version, * retrieved date, raw vs normalized, link). CSS-only so it works inside server components. * * `compact`: link only, no popover — for dense tables. When every row shares one source/dataset the * popover is shown once in the table caption; when provenance genuinely differs per row (e.g. * epidemiology observations from different datasets/years) pass `title` (or let it default to a * dataset · version · retrieved summary) so the detail stays one hover away without the markup. */ export function SourceBadge({ p, className = '', compact = false, title }: { p: ProvenanceInfo; className?: string; compact?: boolean; title?: string | null }) { if (compact) { // The visible slug is the link text; the "Source" column header gives the context and `title` // (dataset · version · retrieved) is exposed as the accessible description. No aria-label // duplicate: every attribute here is repeated hundreds of times per page (HTML + RSC payload). const t = title === null ? undefined : (title ?? (p.dataset || p.datasetVersion ? provenanceTitle(p) : undefined)); const extra = t ? { title: t } : {}; return ( {p.sourceSlug} ); } return ( {p.sourceSlug} Provenance
Source
{p.sourceName ?? p.sourceSlug}
{p.dataset ? ( <>
Dataset
{p.dataset}
) : null} {p.datasetVersion ? ( <>
Version
{p.datasetVersion}
) : null} {p.retrievedAt ? ( <>
Retrieved
{fmtDate(p.retrievedAt)}
) : null}
Layer
{p.layer === 'derived' || p.layer === 'ranked' ? `${p.layer} (computed by CancerIndex)` : p.layer === 'raw' ? 'raw (as published)' : `${p.layer ?? 'normalized'} (units and labels harmonized; values unchanged)`}
{p.evidenceType ? ( <>
Evidence
{p.evidenceType.replace(/_/g, ' ')}
) : null} {p.license ? ( <>
License
{p.license}
) : null} {p.pmid ? ( <>
PMID
{p.pmid}
) : null} {p.ingestRunId ? ( <>
Run
{p.ingestRunId}
) : null}
{p.note ? {p.note} : null} {p.sourceUrl ? ( Open at source ) : null}
); } /** * Table caption used by every dense list: ONE provenance popover for the whole table, the claim * label and a count sentence. Rows then carry compact badges only. */ export function TableProvenance({ p, claim, children, className = '' }: { p: ProvenanceInfo; claim?: React.ReactNode; children?: React.ReactNode; className?: string }) { return ( //
, not

: the popover contains a

, which the HTML parser would close a

on (React #418 in production).

{claim} {children ? {children} : null}
); }