HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { ReactNode } from 'react';2import { Evidence } from '@/components/evidence/evidence';3import { cn } from '@/lib/cn';4import { fmtValue } from '@/lib/format';5import { propertyLabel, URL_KEYS } from '@/lib/site';6import type { Provenance } from '@/lib/types';7import { ProvenanceInline } from './provenance';8import { Missing } from './unavailable';910export type KVRow = { key: string; label?: ReactNode; value?: ReactNode; raw?: unknown; hint?: ReactNode; unit?: string | null };1112/**13 * Dense spec list (<dl>): label column + value + optional per-field provenance. Pass `provenance` (from EntityDetail)14 * to render "Source · tier · observed" under each value when known. Values fall back to `fmtValue(raw, key)`.15 * Pass `slug` (+ optional `entity`) to make every value with provenance an **evidence trigger** that opens the drawer16 * (`components/evidence`). Existing call sites without `slug` render exactly as before.17 */18export function KeyValue({19 rows,20 provenance,21 className,22 dense = false,23 showMissing = false,24 slug,25 entity,26}: {27 rows: KVRow[];28 provenance?: Provenance;29 className?: string;30 dense?: boolean;31 showMissing?: boolean;32 slug?: string;33 entity?: { name?: string; entity_type?: string } | null;34}) {35 const shown = rows.filter((r) => showMissing || r.value !== undefined || (r.raw !== undefined && r.raw !== null && r.raw !== ''));36 if (shown.length === 0) return <p className="text-sm text-ink-3">No structured attributes yet.</p>;37 return (38 <dl className={cn('kv', dense && '[&>div]:py-1.5', className)}>39 {shown.map((r) => {40 const p = provenance?.[r.key];41 const raw = r.raw;42 let value: ReactNode = r.value;43 let display: string | undefined;44 if (value === undefined) {45 if (raw === null || raw === undefined || raw === '') value = <Missing />;46 else if (URL_KEYS.has(r.key) && typeof raw === 'string' && /^https?:\/\//.test(raw))47 value = (48 <a href={raw} className="link break-all" rel="noopener noreferrer" target="_blank">49 {raw.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')}50 </a>51 );52 else {53 display = fmtValue(raw, r.key);54 value = <span className={typeof raw === 'number' ? 'tnum' : undefined}>{display}</span>;55 }56 }57 const canOpen = !!slug && !!p && raw !== null && raw !== undefined && raw !== '' && !(URL_KEYS.has(r.key) && typeof raw === 'string');58 return (59 <div key={r.key}>60 <dt>{r.label ?? propertyLabel(r.key)}</dt>61 <dd>62 <div className="text-ink">63 {canOpen ? (64 <Evidence slug={slug as string} property={r.key} value={raw} display={display} unit={r.unit ?? p?.unit ?? null} fallback={p} entity={entity ?? null}>65 {value}66 </Evidence>67 ) : (68 value69 )}70 </div>71 {r.hint && <div className="mt-0.5 text-xs text-ink-3">{r.hint}</div>}72 {p && <ProvenanceInline p={p} className="mt-0.5" slug={slug} property={r.key} value={raw} entity={entity ?? null} />}73 </dd>74 </div>75 );76 })}77 </dl>78 );79}80