/**
* 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}
);
}