diff --git a/src/components/clinical-dashboard/medication-record-page.tsx b/src/components/clinical-dashboard/medication-record-page.tsx
index 7e2bf80ef..dc1aa5526 100644
--- a/src/components/clinical-dashboard/medication-record-page.tsx
+++ b/src/components/clinical-dashboard/medication-record-page.tsx
@@ -6,17 +6,20 @@ import {
TriangleAlert,
ArrowLeft,
BadgeCheck,
+ BookOpen,
CalendarDays,
ChevronDown,
ClipboardList,
FlaskConical,
+ Gauge,
Lock,
Pill,
ShieldCheck,
+ Sparkles,
type LucideIcon,
} from "lucide-react";
import Link from "next/link";
-import { useMemo, useState } from "react";
+import { useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { BadgeCluster, clinicalBadgeToneClass } from "@/components/clinical-dashboard/clinical-badge";
import { MedicationConsiderations } from "@/components/clinical-dashboard/medication-considerations";
@@ -31,7 +34,7 @@ import {
type MedicationGovernance,
} from "@/lib/medication-badges";
import { medicationDetailTiles, type MedicationRecord, type MedicationSection } from "@/lib/medications";
-import { cn, pageContainer } from "@/components/ui-primitives";
+import { cn, EmptyState, LoadingPanel, pageContainer, toneDanger } from "@/components/ui-primitives";
const sectionIcons: Record = {
dose: CalendarDays,
@@ -43,35 +46,108 @@ const sectionIcons: Record = {
src: BadgeCheck,
};
+// Per-section toned icon tiles. Semantic tones stay reserved (red = contra/safety,
+// amber = risk/caution, green = safe/verified); other sections use the neutral
+// categorical --type-* hues so the list reads with colour without misusing meaning.
+const sectionToneClass: Record = {
+ contra: "border-[color:var(--danger)]/25 bg-[color:var(--danger-soft)] text-[color:var(--danger)]",
+ risk: "border-[color:var(--warning)]/30 bg-[color:var(--warning-soft)] text-[color:var(--warning)]",
+ safe: "border-[color:var(--success)]/25 bg-[color:var(--success-soft)] text-[color:var(--success)]",
+ src: "border-[color:var(--success)]/25 bg-[color:var(--success-soft)] text-[color:var(--success)]",
+ mon: "border-[color:var(--info-border)] bg-[color:var(--info-soft)] text-[color:var(--info)]",
+ dose: "border-[color:var(--type-document-border)] bg-[color:var(--type-document-soft)] text-[color:var(--type-document)]",
+ inter: "border-[color:var(--type-source-border)] bg-[color:var(--type-source-soft)] text-[color:var(--type-source)]",
+};
+const defaultSectionTone =
+ "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]";
+
+// Decorative per-medication identity accent (record.accent is keyed to drug
+// class). Exposed as CSS custom properties and softened with color-mix so it
+// drives washes/rails only — never text — keeping contrast safe in light + dark
+// and staying within the colour contract (semantic colour uses the tokens).
+function medicationAccentStyle(accent: string | undefined): CSSProperties {
+ const base = accent?.trim() || "var(--clinical-accent)";
+ return {
+ "--med-accent": base,
+ "--med-accent-soft": `color-mix(in srgb, ${base} 12%, var(--surface))`,
+ "--med-accent-border": `color-mix(in srgb, ${base} 34%, var(--surface))`,
+ } as CSSProperties;
+}
+
+// Icon + categorical chip per detail tile (index-aligned with medicationDetailTiles:
+// Prescribing answer / Dosing / Dose ceiling / Avoid). The danger tile is toned
+// separately below.
+const detailTileDecor: Array<{ icon: LucideIcon; chip: string }> = [
+ {
+ icon: Sparkles,
+ chip: "border-[color:var(--type-source-border)] bg-[color:var(--type-source-soft)] text-[color:var(--type-source)]",
+ },
+ {
+ icon: CalendarDays,
+ chip: "border-[color:var(--type-document-border)] bg-[color:var(--type-document-soft)] text-[color:var(--type-document)]",
+ },
+ {
+ icon: Gauge,
+ chip: "border-[color:var(--type-service-border)] bg-[color:var(--type-service-soft)] text-[color:var(--type-service)]",
+ },
+ { icon: Ban, chip: "border-[color:var(--danger)]/25 bg-[color:var(--danger-soft)] text-[color:var(--danger)]" },
+];
+
function DetailTile({
label,
value,
meta,
danger = false,
+ icon: Icon,
+ chip,
}: {
label: string;
value: string;
meta?: string;
danger?: boolean;
+ icon: LucideIcon;
+ chip: string;
}) {
return (
-
{label}
+
{value}
{meta ? (
-
+
{meta}
) : null}
@@ -79,18 +155,89 @@ function DetailTile({
);
}
+const detailTabs = [
+ ["summary", "Summary"],
+ ["dosing", "Dosing"],
+ ["safety", "Safety"],
+ ["more", "More"],
+] as const;
+type MedicationTabId = (typeof detailTabs)[number][0];
+
+function SectionTabs({ active, onChange }: { active: MedicationTabId; onChange: (id: MedicationTabId) => void }) {
+ const tabRefs = useRef(new Map
());
+
+ function handleKeyDown(event: ReactKeyboardEvent) {
+ const order = detailTabs.map((tab) => tab[0]);
+ const index = order.indexOf(active);
+ const next =
+ event.key === "ArrowRight"
+ ? order[(index + 1) % order.length]
+ : event.key === "ArrowLeft"
+ ? order[(index - 1 + order.length) % order.length]
+ : event.key === "Home"
+ ? order[0]
+ : event.key === "End"
+ ? order[order.length - 1]
+ : null;
+ if (!next) return;
+ event.preventDefault();
+ if (next !== active) onChange(next);
+ tabRefs.current.get(next)?.focus();
+ }
+
+ return (
+
+ );
+}
+
function SectionCard({ section }: { section: MedicationSection }) {
const Icon = sectionIcons[section.type] || ClipboardList;
+ const toneClass = sectionToneClass[section.type] || defaultSectionTone;
return (
-
-
-
- {section.title}
+
+
+
+
+
+ {section.title}
+
+
+
{title}
+
+ {children}
+
+ );
+}
+
function MedicationAccessPanel({ record }: { record: MedicationRecord }) {
const badges = useMemo(() => medicationAccessBadges(record), [record]);
const fields = useMemo(() => medicationAccessFields(record), [record]);
if (!badges.length && !fields.length) return null;
return (
-
-
-
-
Access
-
+
{fields.length ? (
@@ -148,7 +303,7 @@ function MedicationAccessPanel({ record }: { record: MedicationRecord }) {
) : null}
-
+
);
}
@@ -161,7 +316,7 @@ function MedicationRecordDetail({
}) {
const tiles = useMemo(() => medicationDetailTiles(record), [record]);
const badges = useMemo(() => medicationIdentityBadges(record, governance), [record, governance]);
- const [activeTab, setActiveTab] = useState<"summary" | "dosing" | "safety" | "more">("summary");
+ const [activeTab, setActiveTab] = useState("summary");
const sectionsByTab = useMemo(() => {
const summaryTypes = new Set(["summary", "ind", "form"]);
@@ -179,19 +334,23 @@ function MedicationRecordDetail({
const activeSections = sectionsByTab[activeTab];
return (
-
-
+
+
-
+
-
-
+
+
{record.name}
+
{record.subclass || record.class}
{record.category ? (
<>
@@ -206,85 +365,71 @@ function MedicationRecordDetail({
- {tiles.map((tile) => (
-
- ))}
+ {tiles.map((tile, index) => {
+ const decor = detailTileDecor[index] ?? detailTileDecor[detailTileDecor.length - 1];
+ return (
+
+ );
+ })}
-
- {(
- [
- ["summary", "Summary"],
- ["dosing", "Dosing"],
- ["safety", "Safety"],
- ["more", "More"],
- ] as const
- ).map(([id, label]) => (
-
- ))}
-
+
-
+
{activeSections.length ? (
activeSections.map((section) => (
))
) : (
- No sections in this view.
+
+
+
)}
-
@@ -329,7 +474,9 @@ export function MedicationRecordPage({ slug }: { slug: string }) {
{loading ? (
-
Loading medication reference…
+
+
+
) : error || !data?.record ? (
diff --git a/src/components/clinical-dashboard/patient-profile-panel.tsx b/src/components/clinical-dashboard/patient-profile-panel.tsx
index ecb12f9d1..1e2e01cce 100644
--- a/src/components/clinical-dashboard/patient-profile-panel.tsx
+++ b/src/components/clinical-dashboard/patient-profile-panel.tsx
@@ -77,13 +77,16 @@ function NumberField({
export function PatientProfilePanel({
variant = "full",
+ defaultOpen,
className,
}: {
variant?: "full" | "compact";
+ /** Initial expanded state; falls back to `variant === "full"` when omitted. */
+ defaultOpen?: boolean;
className?: string;
}) {
const { profile, updateField, toggleAllergy, clear, isEmpty } = usePatientProfile();
- const [open, setOpen] = useState(variant === "full");
+ const [open, setOpen] = useState(defaultOpen ?? variant === "full");
const allergies = new Set(profile.allergies ?? []);
return (