A patient chart header with demographics, a high-visibility allergy band, and side-by-side active-problem and medication columns.
npx shadcn@latest add @paragon/ehr-chart-summary"use client";
import * as React from "react";
import { AlertTriangle, User } from "lucide-react";
import { cn } from "@/lib/utils";
export interface ChartAllergy {
substance: string;
/** Reaction severity — drives the chip tint. */
severity?: "mild" | "moderate" | "severe";
}
export interface ChartProblem {
label: string;
/** ICD-10 or local code, shown muted. */
code?: string;
status?: "active" | "resolved";
}
export interface ChartMed {
name: string;
dose: string;
}
export interface EhrChartSummaryProps extends React.ComponentProps<"section"> {
name?: string;
mrn?: string;
/** e.g. "F" · "M" · "X". */
sex?: string;
dob?: string;
age?: number;
allergies?: ChartAllergy[];
problems?: ChartProblem[];
medications?: ChartMed[];
}
const DEFAULTS: Required<
Pick<
EhrChartSummaryProps,
"name" | "mrn" | "sex" | "dob" | "age" | "allergies" | "problems" | "medications"
>
> = {
name: "Dana R. Whitfield",
mrn: "MRN 08823147",
sex: "F",
dob: "1972-03-14",
age: 54,
allergies: [
{ substance: "Penicillin", severity: "severe" },
{ substance: "Sulfa drugs", severity: "moderate" },
{ substance: "Latex", severity: "mild" },
],
problems: [
{ label: "Type 2 diabetes mellitus", code: "E11.9", status: "active" },
{ label: "Essential hypertension", code: "I10", status: "active" },
{ label: "Hyperlipidemia", code: "E78.5", status: "active" },
],
medications: [
{ name: "Metformin", dose: "1000 mg BID" },
{ name: "Lisinopril", dose: "20 mg daily" },
{ name: "Atorvastatin", dose: "40 mg QHS" },
],
};
const severityTint: Record<NonNullable<ChartAllergy["severity"]>, string> = {
mild: "bg-secondary text-secondary-foreground",
moderate: "bg-warning/15 text-warning",
severe: "bg-destructive/12 text-destructive",
};
/**
* A patient chart header: demographics, a high-visibility allergy band, and
* two problem/medication columns at a glance. Static composition — no motion,
* because a chart header is read constantly and must not distract.
*/
export function EhrChartSummary({
name = DEFAULTS.name,
mrn = DEFAULTS.mrn,
sex = DEFAULTS.sex,
dob = DEFAULTS.dob,
age = DEFAULTS.age,
allergies = DEFAULTS.allergies,
problems = DEFAULTS.problems,
medications = DEFAULTS.medications,
className,
...props
}: EhrChartSummaryProps) {
const hasAllergies = allergies.length > 0;
return (
<section
data-slot="ehr-chart-summary"
className={cn(
"w-full overflow-hidden rounded-xl bg-card text-card-foreground shadow-border",
className,
)}
{...props}
>
<header className="flex flex-wrap items-start gap-x-4 gap-y-2 border-b border-border p-4">
<span
aria-hidden
className="flex size-11 shrink-0 items-center justify-center rounded-full bg-secondary text-secondary-foreground"
>
<User className="size-5" />
</span>
<div className="min-w-0 flex-1">
<h3 className="truncate text-base font-semibold">{name}</h3>
<p className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted-foreground tabular-nums">
<span>{mrn}</span>
<span aria-hidden>·</span>
<span>{sex}</span>
<span aria-hidden>·</span>
<span>{age} yr</span>
<span aria-hidden>·</span>
<span>DOB {dob}</span>
</p>
</div>
</header>
<div
className={cn(
"flex items-start gap-2 px-4 py-2.5 text-xs",
hasAllergies
? "bg-destructive/[0.06] text-foreground"
: "bg-success/[0.06] text-foreground",
)}
>
<AlertTriangle
aria-hidden
className={cn(
"mt-px size-3.5 shrink-0",
hasAllergies ? "text-destructive" : "text-success",
)}
/>
<div className="flex flex-wrap items-center gap-1.5">
<span className="font-medium">Allergies</span>
{hasAllergies ? (
allergies.map((a) => (
<span
key={a.substance}
className={cn(
"rounded px-1.5 py-0.5 font-medium",
severityTint[a.severity ?? "mild"],
)}
>
{a.substance}
</span>
))
) : (
<span className="text-muted-foreground">No known allergies</span>
)}
</div>
</div>
<div className="grid grid-cols-1 divide-y divide-border sm:grid-cols-2 sm:divide-x sm:divide-y-0">
<div className="p-4">
<h4 className="text-[11px] font-medium tracking-wide text-muted-foreground uppercase">
Active problems
</h4>
<ul className="mt-2 flex flex-col gap-2">
{problems.map((p) => (
<li key={p.label} className="flex items-baseline gap-2 text-sm">
<span
aria-hidden
className={cn(
"mt-1.5 size-1.5 shrink-0 rounded-full",
p.status === "resolved" ? "bg-muted-foreground/40" : "bg-primary",
)}
/>
<span
className={cn(
"min-w-0 flex-1",
p.status === "resolved" && "text-muted-foreground line-through",
)}
>
{p.label}
</span>
{p.code && (
<span className="shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums">
{p.code}
</span>
)}
</li>
))}
</ul>
</div>
<div className="p-4">
<h4 className="text-[11px] font-medium tracking-wide text-muted-foreground uppercase">
Medications
</h4>
<ul className="mt-2 flex flex-col gap-2">
{medications.map((m) => (
<li
key={m.name}
className="flex items-baseline justify-between gap-3 text-sm"
>
<span className="min-w-0 flex-1 truncate">{m.name}</span>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{m.dose}
</span>
</li>
))}
</ul>
</div>
</div>
</section>
);
}