/** * The explanation design system. Three primitives, used identically everywhere: * * Precision inline dotted-underline text w/ glossary tooltip * small (i) icon w/ glossary tooltip — for labels, * column headers, card titles * same icon w/ ad-hoc content when no glossary key fits * * All tooltips render through one so definitions look the * same on every screen. Definitions live in src/content/glossary.ts. */ import type { ReactNode } from 'react'; import Box from '@mui/material/Box'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { Info } from 'lucide-react'; import { GLOSSARY, type GlossaryEntry } from '../../content/glossary'; // ---------------------------------------------------------------- tooltip body export function GlossaryContent({ entry, compact = false, }: { entry: Pick; compact?: boolean; }) { return ( {entry.term} {entry.short} {!compact ? ( {entry.detail} ) : null} {!compact && entry.caveat ? ( Caveat: {entry.caveat} ) : null} ); } const TOOLTIP_SLOT_PROPS = { tooltip: { sx: { bgcolor: '#1d242c', border: '1px solid #2f3944', boxShadow: '0 4px 16px rgba(0,0,0,0.5)', p: 1.25, }, }, } as const; function resolveEntry( term?: string, title?: ReactNode, detail?: ReactNode ): Pick | null { if (term && GLOSSARY[term]) return GLOSSARY[term]; if (title || detail) { return { term: typeof title === 'string' ? title : '', short: typeof title !== 'string' && title ? String(title) : (detail as string) ?? '', detail: title && detail ? (detail as string) : '', } as GlossaryEntry; } return null; } // ---------------------------------------------------------------- /** * Wraps arbitrary children in a consistently-styled explanation tooltip. * Provide either a glossary `term` key or ad-hoc `title` + `detail`. */ export function ExplainTip({ term, title, detail, compact = false, children, }: { term?: string; title?: string; detail?: string; compact?: boolean; children: React.ReactElement; }) { const entry = term && GLOSSARY[term] ? GLOSSARY[term] : title || detail ? { term: title ?? '', short: detail ?? '', detail: '' } : null; if (!entry) return children; return ( } slotProps={TOOLTIP_SLOT_PROPS} enterDelay={150}> {children} ); } // ---------------------------------------------------------------- /** Small info icon with a glossary (or ad-hoc) tooltip. */ export function InfoDot({ term, title, detail, size = 12, sx, }: { term?: string; title?: string; detail?: string; size?: number; sx?: object; }) { const entry = resolveEntry(term, title, detail); if (!entry) return null; return ( } slotProps={TOOLTIP_SLOT_PROPS} enterDelay={100}> ); } // ---------------------------------------------------------------- /** Inline term with dotted underline; hover reveals the glossary definition. */ export function Term({ k, children, color, }: { k: string; children?: ReactNode; color?: string; }) { const entry = GLOSSARY[k]; const text = children ?? entry?.term ?? k; if (!entry) return {text}; return ( } slotProps={TOOLTIP_SLOT_PROPS} enterDelay={150}> {text} ); } // ---------------------------------------------------------------- helper /** * Column-header label + InfoDot in one span, for table heads: * */ export function HeadCell({ label, term, title, detail }: { label: string; term?: string; title?: string; detail?: string }) { return ( {label} ); }