spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { ExternalLink } from 'lucide-react';3import { fmtDate } from '@/lib/format';45export interface ProvenanceInfo {6 sourceSlug: string;7 sourceName?: string | null;8 dataset?: string | null;9 datasetVersion?: string | null;10 retrievedAt?: Date | string | null;11 sourceUrl?: string | null;12 layer?: 'raw' | 'normalized' | 'canonical' | 'derived' | 'ranked';13 license?: string | null;14 evidenceType?: string | null;15 pmid?: string | null;16 ingestRunId?: string | null;17 note?: string | null;18}1920/** One-line provenance summary for `title` attributes on compact badges (dataset · version · retrieved). */21export function provenanceTitle(p: ProvenanceInfo): string {22 const bits = [p.sourceName ?? p.sourceSlug];23 if (p.dataset) bits.push(p.dataset);24 if (p.datasetVersion) bits.push(`version ${p.datasetVersion}`);25 if (p.retrievedAt) bits.push(`retrieved ${fmtDate(p.retrievedAt)}`);26 if (p.layer === 'derived' || p.layer === 'ranked') bits.push('computed by CancerIndex');27 return bits.join(' · ');28}2930/**31 * Small source badge; hover / focus reveals the provenance popover (source, dataset, version,32 * retrieved date, raw vs normalized, link). CSS-only so it works inside server components.33 *34 * `compact`: link only, no popover — for dense tables. When every row shares one source/dataset the35 * popover is shown once in the table caption; when provenance genuinely differs per row (e.g.36 * epidemiology observations from different datasets/years) pass `title` (or let it default to a37 * dataset · version · retrieved summary) so the detail stays one hover away without the markup.38 */39export function SourceBadge({ p, className = '', compact = false, title }: { p: ProvenanceInfo; className?: string; compact?: boolean; title?: string | null }) {40 if (compact) {41 // The visible slug is the link text; the "Source" column header gives the context and `title`42 // (dataset · version · retrieved) is exposed as the accessible description. No aria-label43 // duplicate: every attribute here is repeated hundreds of times per page (HTML + RSC payload).44 const t = title === null ? undefined : (title ?? (p.dataset || p.datasetVersion ? provenanceTitle(p) : undefined));45 const extra = t ? { title: t } : {};46 return (47 <Link href={`/source/${p.sourceSlug}`} className={`ci-src${className ? ` ${className}` : ''}`} {...extra}>48 {p.sourceSlug}49 </Link>50 );51 }52 return (53 <span className={`ci-pop ${className}`}>54 <Link href={`/source/${p.sourceSlug}`} className="ci-src" aria-label={`Source: ${p.sourceName ?? p.sourceSlug}. Open provenance.`}>55 {p.sourceSlug}56 </Link>57 <span className="ci-pop-panel" role="tooltip">58 <span className="ci-kicker block">Provenance</span>59 <dl className="mt-1 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5">60 <dt className="text-ink-3">Source</dt>61 <dd>{p.sourceName ?? p.sourceSlug}</dd>62 {p.dataset ? (63 <>64 <dt className="text-ink-3">Dataset</dt>65 <dd>{p.dataset}</dd>66 </>67 ) : null}68 {p.datasetVersion ? (69 <>70 <dt className="text-ink-3">Version</dt>71 <dd className="ci-mono">{p.datasetVersion}</dd>72 </>73 ) : null}74 {p.retrievedAt ? (75 <>76 <dt className="text-ink-3">Retrieved</dt>77 <dd>{fmtDate(p.retrievedAt)}</dd>78 </>79 ) : null}80 <dt className="text-ink-3">Layer</dt>81 <dd>{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)`}</dd>82 {p.evidenceType ? (83 <>84 <dt className="text-ink-3">Evidence</dt>85 <dd>{p.evidenceType.replace(/_/g, ' ')}</dd>86 </>87 ) : null}88 {p.license ? (89 <>90 <dt className="text-ink-3">License</dt>91 <dd>{p.license}</dd>92 </>93 ) : null}94 {p.pmid ? (95 <>96 <dt className="text-ink-3">PMID</dt>97 <dd className="ci-mono">{p.pmid}</dd>98 </>99 ) : null}100 {p.ingestRunId ? (101 <>102 <dt className="text-ink-3">Run</dt>103 <dd className="ci-mono break-all">{p.ingestRunId}</dd>104 </>105 ) : null}106 </dl>107 {p.note ? <span className="mt-1 block text-ink-3">{p.note}</span> : null}108 {p.sourceUrl ? (109 <a className="ci-link mt-1.5 inline-flex items-center gap-1" href={p.sourceUrl} target="_blank" rel="noopener noreferrer">110 Open at source <ExternalLink className="h-3 w-3" aria-hidden />111 </a>112 ) : null}113 </span>114 </span>115 );116}117118/**119 * Table caption used by every dense list: ONE provenance popover for the whole table, the claim120 * label and a count sentence. Rows then carry compact badges only.121 */122export function TableProvenance({ p, claim, children, className = '' }: { p: ProvenanceInfo; claim?: React.ReactNode; children?: React.ReactNode; className?: string }) {123 return (124 // <div>, not <p>: the popover contains a <dl>, which the HTML parser would close a <p> on (React #418 in production).125 <div className={`mb-2 flex flex-wrap items-center gap-2 text-[12px] text-ink-3 ${className}`}>126 <SourceBadge p={p} />127 {claim}128 {children ? <span>{children}</span> : null}129 </div>130 );131}132