Healthcare

EHR Chart Summary

A patient chart header with demographics, a high-visibility allergy band, and side-by-side active-problem and medication columns.

Install

npx shadcn@latest add @paragon/ehr-chart-summary

ehr-chart-summary.tsx

"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>
  );
}