mirror of
https://github.com/temetro/temetro.git
synced 2026-09-04 06:45:26 +00:00
Redesign patient cards: 7 focused cards + sparklines; lighten input
- Split the overloaded card into 7 single-topic cards (Summary, Vitals, Labs, Medications, Problems, Allergies & alerts, Visits) so the equal-height row is evenly filled instead of stretched-and-empty. - Add a dependency-free inline-SVG shaded-area Sparkline; Vitals shows a heart-rate trend and Labs a headline-lab trend, fed by new vitalsTrend/ labTrend series in the fixture. - Lighten the chat input's two surfaces so it reads as a raised layer above the new (lighter) page background. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -192,10 +192,10 @@ export function ChatInput({ onSubmit, status, onStop }: ChatInputProps) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submit();
|
submit();
|
||||||
}}
|
}}
|
||||||
className="w-full overflow-hidden rounded-[28px] border border-border/60 bg-[oklch(0.172_0.006_277)] shadow-sm"
|
className="w-full overflow-hidden rounded-[28px] border border-border/60 bg-[oklch(0.195_0.006_277)] shadow-sm"
|
||||||
>
|
>
|
||||||
{/* Top (lighter) card: textarea + toolbar, with a slightly smaller bottom radius */}
|
{/* Top (lighter) card: textarea + toolbar, with a slightly smaller bottom radius */}
|
||||||
<div className="rounded-b-[22px] bg-card">
|
<div className="rounded-b-[22px] bg-[oklch(0.218_0.006_277)]">
|
||||||
<textarea
|
<textarea
|
||||||
aria-label="Message"
|
aria-label="Message"
|
||||||
className="field-sizing-content block max-h-48 min-h-16 w-full resize-none bg-transparent px-5 pt-5 pb-2 text-base text-foreground outline-none placeholder:text-muted-foreground"
|
className="field-sizing-content block max-h-48 min-h-16 w-full resize-none bg-transparent px-5 pt-5 pb-2 text-base text-foreground outline-none placeholder:text-muted-foreground"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +13,8 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import type { AllergySeverity, LabFlag, Patient } from "@/lib/patients";
|
import { Sparkline } from "@/components/chat/sparkline";
|
||||||
|
import type { AllergySeverity, LabFlag, Patient, Trend } from "@/lib/patients";
|
||||||
|
|
||||||
type BadgeVariant = "default" | "secondary" | "destructive" | "outline";
|
type BadgeVariant = "default" | "secondary" | "destructive" | "outline";
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ const sexLabel: Record<Patient["sex"], string> = { F: "Female", M: "Male" };
|
|||||||
// Fixed width so the cards sit in a horizontal scroll row instead of squashing.
|
// Fixed width so the cards sit in a horizontal scroll row instead of squashing.
|
||||||
const rowCard = "w-80 shrink-0 snap-start";
|
const rowCard = "w-80 shrink-0 snap-start";
|
||||||
|
|
||||||
function SectionLabel({ children }: { children: string }) {
|
function SectionLabel({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<p className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
<p className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||||
{children}
|
{children}
|
||||||
@@ -53,6 +56,40 @@ function SectionLabel({ children }: { children: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Stat({ label, value }: { label: string; value: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs text-muted-foreground">{label}</span>
|
||||||
|
<span className="text-foreground">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Row({ label, value }: { label: ReactNode; value: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-baseline justify-between gap-3">
|
||||||
|
<span className="text-foreground">{label}</span>
|
||||||
|
<span className="shrink-0 text-muted-foreground">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TrendBlock({ trend }: { trend: Trend }) {
|
||||||
|
const latest = trend.points.at(-1);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
|
<SectionLabel>{`${trend.label} · last ${trend.points.length}`}</SectionLabel>
|
||||||
|
<span className="text-foreground">
|
||||||
|
{latest}
|
||||||
|
<span className="text-muted-foreground"> {trend.unit}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Sparkline points={trend.points} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SummaryCard({ patient }: { patient: Patient }) {
|
function SummaryCard({ patient }: { patient: Patient }) {
|
||||||
return (
|
return (
|
||||||
<Card className={rowCard} size="sm">
|
<Card className={rowCard} size="sm">
|
||||||
@@ -72,9 +109,121 @@ function SummaryCard({ patient }: { patient: Patient }) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="flex flex-col gap-4">
|
||||||
<span className="text-muted-foreground">Primary care: </span>
|
<div className="grid grid-cols-2 gap-x-3 gap-y-3">
|
||||||
<span className="text-foreground">{patient.pcp}</span>
|
<Stat label="Primary care" value={patient.pcp} />
|
||||||
|
<Stat label="Last seen" value={patient.encounters[0]?.date ?? "—"} />
|
||||||
|
<Stat label="Active meds" value={patient.medications.length} />
|
||||||
|
<Stat label="Open problems" value={patient.problems.length} />
|
||||||
|
</div>
|
||||||
|
{patient.alerts.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{patient.alerts.map((alert) => (
|
||||||
|
<Badge key={alert} variant="outline">
|
||||||
|
{alert}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VitalsCard({ patient }: { patient: Patient }) {
|
||||||
|
const { vitals } = patient;
|
||||||
|
const vitalItems = [
|
||||||
|
{ label: "BP", value: vitals.bp },
|
||||||
|
{ label: "HR", value: vitals.hr },
|
||||||
|
{ label: "Temp", value: vitals.temp },
|
||||||
|
{ label: "SpO₂", value: vitals.spo2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={rowCard} size="sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Vitals</CardTitle>
|
||||||
|
<CardDescription>Taken {vitals.takenAt}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="grid grid-cols-2 gap-x-3 gap-y-3">
|
||||||
|
{vitalItems.map((item) => (
|
||||||
|
<Stat key={item.label} label={item.label} value={item.value} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<TrendBlock trend={patient.vitalsTrend} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabsCard({ patient }: { patient: Patient }) {
|
||||||
|
return (
|
||||||
|
<Card className={rowCard} size="sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Labs</CardTitle>
|
||||||
|
<CardDescription>As of {patient.labs[0]?.takenAt ?? "—"}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{patient.labs.map((lab) => (
|
||||||
|
<Row
|
||||||
|
key={lab.name}
|
||||||
|
label={lab.name}
|
||||||
|
value={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{lab.value}
|
||||||
|
<Badge className="capitalize" variant={labFlagVariant[lab.flag]}>
|
||||||
|
{lab.flag}
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<TrendBlock trend={patient.labTrend} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MedicationsCard({ patient }: { patient: Patient }) {
|
||||||
|
return (
|
||||||
|
<Card className={rowCard} size="sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Medications</CardTitle>
|
||||||
|
<CardDescription>{patient.medications.length} active</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-2">
|
||||||
|
{patient.medications.map((med) => (
|
||||||
|
<Row
|
||||||
|
key={med.name}
|
||||||
|
label={med.name}
|
||||||
|
value={`${med.dose} · ${med.frequency}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProblemsCard({ patient }: { patient: Patient }) {
|
||||||
|
return (
|
||||||
|
<Card className={rowCard} size="sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Problems</CardTitle>
|
||||||
|
<CardDescription>{patient.problems.length} active</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-2">
|
||||||
|
{patient.problems.map((problem) => (
|
||||||
|
<Row
|
||||||
|
key={problem.label}
|
||||||
|
label={problem.label}
|
||||||
|
value={`since ${problem.since}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -102,24 +251,26 @@ function AllergiesCard({ patient }: { patient: Patient }) {
|
|||||||
<p className="text-muted-foreground">No known allergies.</p>
|
<p className="text-muted-foreground">No known allergies.</p>
|
||||||
) : (
|
) : (
|
||||||
patient.allergies.map((allergy) => (
|
patient.allergies.map((allergy) => (
|
||||||
<div
|
<Row
|
||||||
className="flex items-center justify-between gap-3"
|
|
||||||
key={allergy.substance}
|
key={allergy.substance}
|
||||||
>
|
label={
|
||||||
<span className="text-foreground">
|
<>
|
||||||
{allergy.substance}
|
{allergy.substance}
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{" "}
|
{" "}
|
||||||
— {allergy.reaction}
|
— {allergy.reaction}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</>
|
||||||
<Badge
|
}
|
||||||
className="capitalize"
|
value={
|
||||||
variant={severityVariant[allergy.severity]}
|
<Badge
|
||||||
>
|
className="capitalize"
|
||||||
{allergy.severity}
|
variant={severityVariant[allergy.severity]}
|
||||||
</Badge>
|
>
|
||||||
</div>
|
{allergy.severity}
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -128,95 +279,30 @@ function AllergiesCard({ patient }: { patient: Patient }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MedicationsCard({ patient }: { patient: Patient }) {
|
function VisitsCard({ patient }: { patient: Patient }) {
|
||||||
return (
|
return (
|
||||||
<Card className={rowCard} size="sm">
|
<Card className={rowCard} size="sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Medications & problems</CardTitle>
|
<CardTitle>Recent visits</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-3">
|
||||||
<div className="flex flex-col gap-2">
|
{patient.encounters.map((encounter) => (
|
||||||
<SectionLabel>Active medications</SectionLabel>
|
<div
|
||||||
{patient.medications.map((med) => (
|
className="flex flex-col gap-0.5"
|
||||||
<div className="flex items-baseline justify-between gap-3" key={med.name}>
|
key={encounter.date + encounter.type}
|
||||||
<span className="text-foreground">{med.name}</span>
|
>
|
||||||
<span className="text-muted-foreground">
|
<div className="flex items-baseline justify-between gap-3">
|
||||||
{med.dose} · {med.frequency}
|
<span className="text-foreground">{encounter.type}</span>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{encounter.date}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<span className="text-muted-foreground">{encounter.summary}</span>
|
||||||
</div>
|
<span className="text-xs text-muted-foreground">
|
||||||
<Separator />
|
{encounter.provider}
|
||||||
<div className="flex flex-col gap-2">
|
</span>
|
||||||
<SectionLabel>Problem list</SectionLabel>
|
</div>
|
||||||
{patient.problems.map((problem) => (
|
))}
|
||||||
<div
|
|
||||||
className="flex items-baseline justify-between gap-3"
|
|
||||||
key={problem.label}
|
|
||||||
>
|
|
||||||
<span className="text-foreground">{problem.label}</span>
|
|
||||||
<span className="text-muted-foreground">since {problem.since}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VitalsCard({ patient }: { patient: Patient }) {
|
|
||||||
const { vitals } = patient;
|
|
||||||
const vitalItems = [
|
|
||||||
{ label: "BP", value: vitals.bp },
|
|
||||||
{ label: "HR", value: vitals.hr },
|
|
||||||
{ label: "Temp", value: vitals.temp },
|
|
||||||
{ label: "SpO₂", value: vitals.spo2 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={rowCard} size="sm">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Vitals, labs & visits</CardTitle>
|
|
||||||
<CardDescription>Vitals taken {vitals.takenAt}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
||||||
{vitalItems.map((item) => (
|
|
||||||
<div className="flex flex-col gap-0.5" key={item.label}>
|
|
||||||
<span className="text-xs text-muted-foreground">{item.label}</span>
|
|
||||||
<span className="text-foreground">{item.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<SectionLabel>Recent labs</SectionLabel>
|
|
||||||
{patient.labs.map((lab) => (
|
|
||||||
<div className="flex items-center justify-between gap-3" key={lab.name}>
|
|
||||||
<span className="text-foreground">{lab.name}</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-muted-foreground">{lab.value}</span>
|
|
||||||
<Badge className="capitalize" variant={labFlagVariant[lab.flag]}>
|
|
||||||
{lab.flag}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<SectionLabel>Recent visits</SectionLabel>
|
|
||||||
{patient.encounters.map((encounter) => (
|
|
||||||
<div className="flex flex-col gap-0.5" key={encounter.date + encounter.type}>
|
|
||||||
<div className="flex items-baseline justify-between gap-3">
|
|
||||||
<span className="text-foreground">{encounter.type}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{encounter.date}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground">{encounter.summary}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{encounter.provider}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -231,20 +317,26 @@ function LoadingCards() {
|
|||||||
<Skeleton className="size-10 rounded-full" />
|
<Skeleton className="size-10 rounded-full" />
|
||||||
<div className="grid flex-1 gap-1.5">
|
<div className="grid flex-1 gap-1.5">
|
||||||
<Skeleton className="h-4 w-40" />
|
<Skeleton className="h-4 w-40" />
|
||||||
<Skeleton className="h-3 w-56" />
|
<Skeleton className="h-3 w-52" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-2 gap-3">
|
||||||
|
{[0, 1, 2, 3].map((cell) => (
|
||||||
|
<Skeleton className="h-8 w-full" key={cell} />
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{[0, 1, 2].map((card) => (
|
{[0, 1, 2, 3, 4, 5].map((card) => (
|
||||||
<Card className={rowCard} key={card} size="sm">
|
<Card className={rowCard} key={card} size="sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Skeleton className="h-4 w-44" />
|
<Skeleton className="h-4 w-28" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-2.5">
|
<CardContent className="flex flex-col gap-2.5">
|
||||||
{[0, 1, 2].map((row) => (
|
{[0, 1, 2].map((row) => (
|
||||||
<Skeleton className="h-3.5 w-full" key={row} />
|
<Skeleton className="h-3.5 w-full" key={row} />
|
||||||
))}
|
))}
|
||||||
|
{card % 2 === 1 && <Skeleton className="mt-1.5 h-10 w-full" />}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -272,9 +364,12 @@ export function PatientResult({ status, fileNumber, patient }: PatientResultProp
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SummaryCard patient={patient} />
|
<SummaryCard patient={patient} />
|
||||||
<AllergiesCard patient={patient} />
|
|
||||||
<MedicationsCard patient={patient} />
|
|
||||||
<VitalsCard patient={patient} />
|
<VitalsCard patient={patient} />
|
||||||
|
<LabsCard patient={patient} />
|
||||||
|
<MedicationsCard patient={patient} />
|
||||||
|
<ProblemsCard patient={patient} />
|
||||||
|
<AllergiesCard patient={patient} />
|
||||||
|
<VisitsCard patient={patient} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useId } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// A tiny dependency-free trend chart: a line with a soft shaded fill beneath it.
|
||||||
|
// Stretches to its container width; colored via `currentColor` (default text-primary).
|
||||||
|
export function Sparkline({
|
||||||
|
points,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
points: number[];
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const gradientId = useId();
|
||||||
|
|
||||||
|
if (points.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = 100;
|
||||||
|
const top = 3;
|
||||||
|
const bottom = 29;
|
||||||
|
const baseline = 32;
|
||||||
|
const min = Math.min(...points);
|
||||||
|
const max = Math.max(...points);
|
||||||
|
const range = max - min;
|
||||||
|
|
||||||
|
const coords = points.map((value, index) => {
|
||||||
|
const x =
|
||||||
|
points.length === 1 ? width / 2 : (index / (points.length - 1)) * width;
|
||||||
|
const y =
|
||||||
|
range === 0
|
||||||
|
? (top + bottom) / 2
|
||||||
|
: bottom - ((value - min) / range) * (bottom - top);
|
||||||
|
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const line = coords.map((c, i) => `${i === 0 ? "M" : "L"}${c}`).join(" ");
|
||||||
|
const area = `${line} L${width},${baseline} L0,${baseline} Z`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("h-10 w-full text-primary", className)}
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
viewBox={`0 0 ${width} ${baseline}`}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="currentColor" stopOpacity="0.28" />
|
||||||
|
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path d={area} fill={`url(#${gradientId})`} stroke="none" />
|
||||||
|
<path
|
||||||
|
d={line}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,6 +45,14 @@ export type Encounter = {
|
|||||||
summary: string;
|
summary: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A short series for a sparkline. `points` are most-recent-last, so the
|
||||||
|
// latest reading is points.at(-1).
|
||||||
|
export type Trend = {
|
||||||
|
label: string;
|
||||||
|
unit: string;
|
||||||
|
points: number[];
|
||||||
|
};
|
||||||
|
|
||||||
export type Patient = {
|
export type Patient = {
|
||||||
fileNumber: string; // MRN / file number, e.g. "10293"
|
fileNumber: string; // MRN / file number, e.g. "10293"
|
||||||
name: string;
|
name: string;
|
||||||
@@ -58,7 +66,9 @@ export type Patient = {
|
|||||||
medications: Medication[];
|
medications: Medication[];
|
||||||
problems: Problem[];
|
problems: Problem[];
|
||||||
vitals: Vitals;
|
vitals: Vitals;
|
||||||
|
vitalsTrend: Trend; // headline vital plotted as a sparkline
|
||||||
labs: Lab[];
|
labs: Lab[];
|
||||||
|
labTrend: Trend; // headline lab plotted as a sparkline
|
||||||
encounters: Encounter[];
|
encounters: Encounter[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,12 +105,22 @@ const PATIENTS: Record<string, Patient> = {
|
|||||||
spo2: "97%",
|
spo2: "97%",
|
||||||
takenAt: "May 28, 2026",
|
takenAt: "May 28, 2026",
|
||||||
},
|
},
|
||||||
|
vitalsTrend: {
|
||||||
|
label: "Heart rate",
|
||||||
|
unit: "bpm",
|
||||||
|
points: [72, 75, 74, 79, 83, 80, 78],
|
||||||
|
},
|
||||||
labs: [
|
labs: [
|
||||||
{ name: "HbA1c", value: "8.2 %", flag: "high", takenAt: "May 20, 2026" },
|
{ name: "HbA1c", value: "8.2 %", flag: "high", takenAt: "May 20, 2026" },
|
||||||
{ name: "eGFR", value: "52 mL/min", flag: "low", takenAt: "May 20, 2026" },
|
{ name: "eGFR", value: "52 mL/min", flag: "low", takenAt: "May 20, 2026" },
|
||||||
{ name: "Potassium", value: "4.4 mmol/L", flag: "normal", takenAt: "May 20, 2026" },
|
{ name: "Potassium", value: "4.4 mmol/L", flag: "normal", takenAt: "May 20, 2026" },
|
||||||
{ name: "INR", value: "2.6", flag: "normal", takenAt: "May 20, 2026" },
|
{ name: "INR", value: "2.6", flag: "normal", takenAt: "May 20, 2026" },
|
||||||
],
|
],
|
||||||
|
labTrend: {
|
||||||
|
label: "HbA1c",
|
||||||
|
unit: "%",
|
||||||
|
points: [9.4, 9.1, 8.8, 8.6, 8.2],
|
||||||
|
},
|
||||||
encounters: [
|
encounters: [
|
||||||
{
|
{
|
||||||
date: "May 28, 2026",
|
date: "May 28, 2026",
|
||||||
@@ -151,12 +171,22 @@ const PATIENTS: Record<string, Patient> = {
|
|||||||
spo2: "92%",
|
spo2: "92%",
|
||||||
takenAt: "May 31, 2026",
|
takenAt: "May 31, 2026",
|
||||||
},
|
},
|
||||||
|
vitalsTrend: {
|
||||||
|
label: "Heart rate",
|
||||||
|
unit: "bpm",
|
||||||
|
points: [88, 91, 96, 99, 97, 95, 94],
|
||||||
|
},
|
||||||
labs: [
|
labs: [
|
||||||
{ name: "WBC", value: "14.8 ×10⁹/L", flag: "high", takenAt: "May 31, 2026" },
|
{ name: "WBC", value: "14.8 ×10⁹/L", flag: "high", takenAt: "May 31, 2026" },
|
||||||
{ name: "CRP", value: "112 mg/L", flag: "critical", takenAt: "May 31, 2026" },
|
{ name: "CRP", value: "112 mg/L", flag: "critical", takenAt: "May 31, 2026" },
|
||||||
{ name: "Lactate", value: "1.8 mmol/L", flag: "normal", takenAt: "May 31, 2026" },
|
{ name: "Lactate", value: "1.8 mmol/L", flag: "normal", takenAt: "May 31, 2026" },
|
||||||
{ name: "Hemoglobin", value: "11.9 g/dL", flag: "low", takenAt: "May 31, 2026" },
|
{ name: "Hemoglobin", value: "11.9 g/dL", flag: "low", takenAt: "May 31, 2026" },
|
||||||
],
|
],
|
||||||
|
labTrend: {
|
||||||
|
label: "CRP",
|
||||||
|
unit: "mg/L",
|
||||||
|
points: [38, 64, 98, 126, 112],
|
||||||
|
},
|
||||||
encounters: [
|
encounters: [
|
||||||
{
|
{
|
||||||
date: "May 30, 2026",
|
date: "May 30, 2026",
|
||||||
@@ -197,11 +227,21 @@ const PATIENTS: Record<string, Patient> = {
|
|||||||
spo2: "99%",
|
spo2: "99%",
|
||||||
takenAt: "May 26, 2026",
|
takenAt: "May 26, 2026",
|
||||||
},
|
},
|
||||||
|
vitalsTrend: {
|
||||||
|
label: "Heart rate",
|
||||||
|
unit: "bpm",
|
||||||
|
points: [78, 80, 79, 83, 85, 84, 82],
|
||||||
|
},
|
||||||
labs: [
|
labs: [
|
||||||
{ name: "TSH", value: "2.1 mIU/L", flag: "normal", takenAt: "May 19, 2026" },
|
{ name: "TSH", value: "2.1 mIU/L", flag: "normal", takenAt: "May 19, 2026" },
|
||||||
{ name: "Hemoglobin", value: "11.2 g/dL", flag: "low", takenAt: "May 19, 2026" },
|
{ name: "Hemoglobin", value: "11.2 g/dL", flag: "low", takenAt: "May 19, 2026" },
|
||||||
{ name: "Fasting glucose", value: "84 mg/dL", flag: "normal", takenAt: "May 19, 2026" },
|
{ name: "Fasting glucose", value: "84 mg/dL", flag: "normal", takenAt: "May 19, 2026" },
|
||||||
],
|
],
|
||||||
|
labTrend: {
|
||||||
|
label: "Hemoglobin",
|
||||||
|
unit: "g/dL",
|
||||||
|
points: [12.4, 12.0, 11.7, 11.4, 11.2],
|
||||||
|
},
|
||||||
encounters: [
|
encounters: [
|
||||||
{
|
{
|
||||||
date: "May 26, 2026",
|
date: "May 26, 2026",
|
||||||
|
|||||||
Reference in New Issue
Block a user