diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
index 5e6404c0c..14023d501 100644
--- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
+++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
@@ -2,12 +2,18 @@
import {
Activity,
+ Ban,
CalendarDays,
CheckCircle2,
ChevronRight,
Lock,
Pill,
+ SearchX,
+ ShieldAlert,
ShieldCheck,
+ Sparkles,
+ Target,
+ TriangleAlert,
UserRound,
type LucideIcon,
} from "lucide-react";
@@ -18,9 +24,17 @@ import { ModeHomeTemplate, ModeHomeVerificationFooter } from "@/components/mode-
import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search-results-header-band";
import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context";
import { useMedicationCatalog } from "@/components/clinical-dashboard/use-medication-catalog";
+import {
+ BadgeCluster,
+ ClinicalBadge,
+ type ClinicalBadgeItem,
+ type ClinicalBadgeTone,
+} from "@/components/clinical-dashboard/clinical-badge";
+import { medicationIdentityBadges, type MedicationRecord } from "@/lib/medications";
import { medicationMatchesCommandScopes } from "@/lib/search-command-surface";
+import { SEMANTIC_TONE_META } from "@/lib/semantic-tone";
import { isDeployedClinicalKb } from "@/lib/deployed-app";
-import { cn } from "@/components/ui-primitives";
+import { cn, EmptyState } from "@/components/ui-primitives";
type MedicationPrescribingWorkspaceProps = {
query: string;
@@ -46,17 +60,23 @@ type MedicationResult = {
dose: string;
ceiling: string;
action: string;
+ actionTone: "danger" | "warning" | "neutral";
tone: "teal" | "blue" | "slate";
href?: string;
};
+type MedicationRow = {
+ result: MedicationResult;
+ badges: ClinicalBadgeItem[];
+};
+
type MedicationResultFilter = "best" | "indication" | "safety" | "monitoring";
-const medicationResultFilters: Array<{ id: MedicationResultFilter; label: string }> = [
- { id: "best", label: "Best" },
- { id: "indication", label: "Indication" },
- { id: "safety", label: "Safety" },
- { id: "monitoring", label: "Monitor" },
+const medicationResultFilters: Array<{ id: MedicationResultFilter; label: string; icon: LucideIcon }> = [
+ { id: "best", label: "Best", icon: Sparkles },
+ { id: "indication", label: "Indication", icon: Target },
+ { id: "safety", label: "Safety", icon: ShieldAlert },
+ { id: "monitoring", label: "Monitor", icon: Activity },
];
const medicationCapabilities: Capability[] = [
@@ -199,15 +219,26 @@ function MedicationHome({
function resultMatchesFilter(result: MedicationResult, filter: MedicationResultFilter) {
if (filter === "best") return true;
if (filter === "indication") return result.match !== "Related match";
- if (filter === "safety") return /check|avoid|caution|ceiling|max/i.test(result.action);
- return /monitor|level|review|renal|hepatic/i.test(`${result.action} ${result.dose} ${result.ceiling}`);
+ // actionTone is source-derived (contraindication vs caution vs monitoring content),
+ // so it is a stronger signal than the text heuristics — any row whose action shows
+ // a safety icon (danger or warning) must be reachable through the Safety chip. The
+ // chips are lenses, not partitions, so warning rows may also appear under Monitor.
+ if (filter === "safety") {
+ return result.actionTone !== "neutral" || /check|avoid|caution|ceiling|max/i.test(result.action);
+ }
+ return (
+ result.actionTone === "warning" ||
+ /monitor|level|review|renal|hepatic/i.test(`${result.action} ${result.dose} ${result.ceiling}`)
+ );
}
function FilterStrip({
activeFilter,
+ counts,
onFilterChange,
}: {
activeFilter: MedicationResultFilter;
+ counts: Record;
onFilterChange: (filter: MedicationResultFilter) => void;
}) {
return (
@@ -215,22 +246,32 @@ function FilterStrip({
className="flex gap-1.5 overflow-x-auto pb-1 [-webkit-overflow-scrolling:touch]"
aria-label="Medication result filters"
>
- {medicationResultFilters.map((filter) => (
-
- ))}
+ {medicationResultFilters.map((filter) => {
+ const active = activeFilter === filter.id;
+ const Icon = filter.icon;
+ return (
+
+ );
+ })}
);
}
@@ -240,21 +281,53 @@ function ResultToneIcon({ result }: { result: MedicationResult }) {
return ;
}
+const matchBadgeTone: Record = {
+ teal: "clinical",
+ blue: "info",
+ slate: "neutral",
+};
+
function ResultMatchBadge({ result }: { result: MedicationResult }) {
+ return ;
+}
+
+// Small leading icon for the prescribing-action text: contraindication content is
+// a quiet stop signal (Ban, danger colour), monitoring content a check-first
+// caution (TriangleAlert, warning colour). The text itself stays heading-coloured
+// so red remains reserved and readable per the badge governance guide.
+function ActionToneIcon({ tone, className }: { tone: MedicationResult["actionTone"]; className?: string }) {
+ if (tone === "neutral") return null;
+ const Icon = tone === "danger" ? Ban : TriangleAlert;
return (
-
-
- {result.match}
-
+ <>
+ {SEMANTIC_TONE_META[tone].ariaPrefix}:
+
+ >
+ );
+}
+
+// Highlight the first query token inside the medication name when it is a plain
+// substring match; synonym/expanded matches simply render unhighlighted.
+function HighlightedName({ text, term }: { text: string; term: string }) {
+ const token = term.trim().split(/\s+/)[0] ?? "";
+ if (token.length < 2) return <>{text}>;
+ const index = text.toLowerCase().indexOf(token.toLowerCase());
+ if (index < 0) return <>{text}>;
+ return (
+ <>
+ {text.slice(0, index)}
+
+ {text.slice(index, index + token.length)}
+
+ {text.slice(index + token.length)}
+ >
);
}
@@ -282,26 +355,52 @@ function MedicationResults({
const command = useSearchCommand();
const catalog = useMedicationCatalog(query);
const [activeFilter, setActiveFilter] = useState("best");
- const visibleMedicationResults = useMemo(() => {
- const sourceResults =
- catalog.data?.matches?.map((match) => match.result) ??
- (catalog.data?.records ?? []).slice(0, 12).map((record) => ({
- id: record.slug,
- name: record.name,
- indication: record.subclass || record.category,
- match: "Catalogue match",
- dose: "See reference",
- ceiling: "See reference",
- action: "Open full prescribing reference.",
- tone: "slate" as const,
- href: `/medications/${record.slug}`,
- }));
- const filtered = sourceResults.filter((result) => resultMatchesFilter(result, activeFilter));
+ const { rows, counts, totalAvailable } = useMemo(() => {
+ const governance = catalog.data?.governance;
+ const toRow = (result: MedicationResult, medication?: MedicationRecord): MedicationRow => ({
+ result,
+ badges: medication ? medicationIdentityBadges(medication, governance?.[medication.slug]) : [],
+ });
+ const sourceRows =
+ catalog.data?.matches?.map((match) => toRow(match.result, match.medication)) ??
+ (catalog.data?.records ?? []).slice(0, 12).map((record) =>
+ toRow(
+ {
+ id: record.slug,
+ name: record.name,
+ indication: record.subclass || record.category,
+ match: "Catalogue match",
+ dose: "See reference",
+ ceiling: "See reference",
+ action: "Open full prescribing reference.",
+ actionTone: "neutral" as const,
+ tone: "slate" as const,
+ href: `/medications/${record.slug}`,
+ },
+ record,
+ ),
+ );
const scopes = command?.commandScopes ?? [];
- if (!scopes.length) return filtered;
- return filtered.filter((result) => medicationMatchesCommandScopes(result, scopes));
+ const scoped = scopes.length
+ ? sourceRows.filter((row) => medicationMatchesCommandScopes(row.result, scopes))
+ : sourceRows;
+ const filterCounts: Record = { best: 0, indication: 0, safety: 0, monitoring: 0 };
+ for (const row of scoped) {
+ for (const filter of medicationResultFilters) {
+ if (resultMatchesFilter(row.result, filter.id)) filterCounts[filter.id] += 1;
+ }
+ }
+ return {
+ rows: scoped.filter((row) => resultMatchesFilter(row.result, activeFilter)),
+ counts: filterCounts,
+ totalAvailable: scoped.length,
+ };
}, [activeFilter, catalog.data, command?.commandScopes]);
- const resultCount = visibleMedicationResults.length;
+ const resultCount = rows.length;
+ // The match-quality badge only earns its slot when it differentiates: hide it on
+ // "Exact clinical fit" rows when every visible row says the same thing.
+ const showMatchBadge = useMemo(() => new Set(rows.map((row) => row.result.match)).size > 1, [rows]);
+ const activeFilterLabel = medicationResultFilters.find((filter) => filter.id === activeFilter)?.label ?? "filtered";
return (
@@ -320,7 +419,7 @@ function MedicationResults({
-
+
{catalog.loading ? (
Loading medication catalogue…
@@ -330,9 +429,34 @@ function MedicationResults({
) : null}
- {!catalog.loading && !catalog.error ? (
+ {!catalog.loading && !catalog.error && resultCount === 0 ? (
+ totalAvailable > 0 ? (
+
+
+
+
+ ) : (
+
+ )
+ ) : null}
+
+ {!catalog.loading && !catalog.error && resultCount > 0 ? (
-
+
Medication
Dose
Ceiling
@@ -340,40 +464,49 @@ function MedicationResults({
Open
- {visibleMedicationResults.map((result, index) => {
+ {rows.map((row, index) => {
+ const result = row.result;
const selected = index === 0 && Boolean(query.trim());
const rowClassName = cn(
- "grid w-full grid-cols-[minmax(16rem,1.15fr)_minmax(6.5rem,0.42fr)_minmax(8rem,0.48fr)_minmax(16rem,1fr)_2rem] items-center gap-2.5 px-4 py-2.5 text-left transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-[color:var(--focus)]",
+ "group grid w-full grid-cols-[minmax(16rem,1.15fr)_minmax(6.5rem,0.42fr)_minmax(8rem,0.48fr)_minmax(16rem,1fr)_2rem] items-center gap-2.5 px-4 py-2.5 text-left transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-[color:var(--focus)]",
selected
- ? "bg-[color:var(--clinical-accent-soft)]/35 ring-1 ring-inset ring-[color:var(--clinical-accent)]/35"
+ ? "bg-[color:var(--clinical-accent-soft)]/35 shadow-[inset_2px_0_0_var(--clinical-accent)] ring-1 ring-inset ring-[color:var(--clinical-accent)]/35"
: result.href
? "hover:bg-[color:var(--surface-subtle)]"
: "cursor-default opacity-80",
);
const rowContent = (
<>
-
+
-
+
- {result.name}
+
{result.indication}
-
-
-
-
+ {showMatchBadge || result.match !== "Exact clinical fit" || row.badges.length > 0 ? (
+
+ {showMatchBadge || result.match !== "Exact clinical fit" ? (
+
+ ) : null}
+
+
+ ) : null}
+
+
+
+ {result.dose}
- {result.dose}
-
- {result.action}
+
+
+ {result.action}
{result.href ? (
) : (
@@ -412,7 +545,8 @@ function MedicationResults({
) : null}
- {visibleMedicationResults.map((result, index) => {
+ {rows.map((row, index) => {
+ const result = row.result;
const selected = index === 0 && Boolean(query.trim());
const cardClassName = cn(
"min-w-0 w-full rounded-lg border bg-[color:var(--surface-raised)] p-2 text-left shadow-[var(--shadow-inset)] transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]",
@@ -428,18 +562,24 @@ function MedicationResults({
- {result.name}
+
{result.indication}
-
-
-
+ {showMatchBadge || result.match !== "Exact clinical fit" || row.badges.length > 0 ? (
+
+ {showMatchBadge || result.match !== "Exact clinical fit" ? (
+
+ ) : null}
+
+
+ ) : null}
- {result.dose}
+ {result.dose}
+
{result.action}
diff --git a/src/components/clinical-dashboard/use-medication-catalog.ts b/src/components/clinical-dashboard/use-medication-catalog.ts
index 074812a5c..1daf2f744 100644
--- a/src/components/clinical-dashboard/use-medication-catalog.ts
+++ b/src/components/clinical-dashboard/use-medication-catalog.ts
@@ -16,6 +16,7 @@ type MedicationCatalogResponse = {
records: MedicationRecord[];
matches?: MedicationCatalogMatch[];
total: number;
+ governance?: Record
;
demoMode?: boolean;
};
diff --git a/src/lib/medications.ts b/src/lib/medications.ts
index 56965521c..d5ad31e2e 100644
--- a/src/lib/medications.ts
+++ b/src/lib/medications.ts
@@ -55,6 +55,8 @@ export type MedicationSearchMatch = {
export type MedicationResultTone = "teal" | "blue" | "slate";
+export type MedicationActionTone = "danger" | "warning" | "neutral";
+
export type MedicationSearchResult = {
id: string;
name: string;
@@ -63,6 +65,7 @@ export type MedicationSearchResult = {
dose: string;
ceiling: string;
action: string;
+ actionTone: MedicationActionTone;
tone: MedicationResultTone;
href: string;
};
@@ -144,28 +147,77 @@ export function medicationIndication(record: MedicationRecord) {
);
}
+// Take the first sentence without mangling decimals ("1.5 mg") or common
+// abbreviations ("e.g.", "i.e.", "etc.") the naive split(".")[0] used to cut
+// mid-parenthesis ("Any hepatic impairment (e").
+export function firstClinicalSentence(value: string): string {
+ const text = value.trim();
+ const periods = /\./g;
+ let match: RegExpExecArray | null;
+ while ((match = periods.exec(text))) {
+ const next = text[match.index + 1];
+ if (next !== undefined && !/\s/.test(next)) continue;
+ const before = text.slice(0, match.index);
+ if (/(?:^|[\s(])(?:e\.g|i\.e|etc|vs|approx)$/i.test(before)) continue;
+ return before.trim();
+ }
+ return text;
+}
+
export function medicationUsualDose(record: MedicationRecord) {
const quickDose = quickValue(record, "usual dose");
- if (quickDose) return quickDose.replace(/\*\*/g, "").split(".")[0]?.trim() ?? quickDose;
+ if (quickDose) return firstClinicalSentence(quickDose.replace(/\*\*/g, "")) || quickDose;
const doseRow = sectionByType(record, "dose")?.rows[0];
- return doseRow?.val?.replace(/\*\*/g, "").split(".")[0]?.trim() ?? "See dosing";
+ const doseValue = doseRow?.val?.replace(/\*\*/g, "");
+ return doseValue ? firstClinicalSentence(doseValue) || "See dosing" : "See dosing";
}
export function medicationCeiling(record: MedicationRecord) {
return statValue(record, "max dose") || statValue(record, "ceiling") || "See reference";
}
+// The action tone is derived from which source field supplied the text (not from
+// output-text heuristics): avoid/contraindication content is a hard stop (danger),
+// monitoring/laboratory content is a check-first caution (warning), and summary or
+// fallback text is neutral reference material.
+export function medicationActionDetail(record: MedicationRecord): { text: string; tone: MedicationActionTone } {
+ const sources: Array<{ raw: string; tone: MedicationActionTone }> = [
+ { raw: quickValue(record, "avoid"), tone: "danger" },
+ { raw: firstRowValue(record, "contra", "absolute"), tone: "danger" },
+ { raw: firstRowValue(record, "summary", "clinical focus"), tone: "neutral" },
+ { raw: firstRowValue(record, "mon", "laboratory"), tone: "warning" },
+ ];
+ const picked = sources.find((source) => source.raw) ?? {
+ raw: "Review full prescribing reference.",
+ tone: "neutral" as const,
+ };
+ const text = firstClinicalSentence(picked.raw.replace(/\*\*/g, "")) || "Review full prescribing reference";
+ return { text, tone: picked.tone === "danger" ? avoidTextTone(text) : picked.tone };
+}
+
+// Avoid/contraindication fields are heterogeneous: hard stops ("Contraindicated
+// in ...", "Severe respiratory depression, paralytic ileus"), explicit
+// no-contraindication statements ("NONE — ...") and caution-only guidance
+// ("Pregnancy Category B2", "requires pharmacist review and dose reduction").
+// Only hard stops may carry the danger icon / "Do not use" prefix. Condition
+// lists without any keyword stay danger — under-warning is the failure mode to
+// avoid — so downgrades are keyed to explicit none/caution phrasing only.
+const HARD_STOP_PATTERN = /contraindicat|do not\b|avoid\b|hypersensitiv|anaphylax|never\b|must not/i;
+const CAUTION_ONLY_PATTERN =
+ /pregnancy category|\bcategory [ab]\d?\b|pharmacist|dose reduction|reduce dose|requires?\b[^.]*\breview|generally (?:considered )?safe/i;
+
+function avoidTextTone(text: string): MedicationActionTone {
+ if (/^(?:none\b|no\s+(?:absolute\s+)?contraindication)/i.test(text)) {
+ return /caution/i.test(text) ? "warning" : "neutral";
+ }
+ if (!HARD_STOP_PATTERN.test(text) && CAUTION_ONLY_PATTERN.test(text)) {
+ return "warning";
+ }
+ return "danger";
+}
+
export function medicationAction(record: MedicationRecord) {
- return (
- quickValue(record, "avoid") ||
- firstRowValue(record, "contra", "absolute") ||
- firstRowValue(record, "summary", "clinical focus") ||
- firstRowValue(record, "mon", "laboratory") ||
- "Review full prescribing reference."
- )
- .replace(/\*\*/g, "")
- .split(".")[0]
- ?.trim();
+ return medicationActionDetail(record).text;
}
export function medicationResultTone(record: MedicationRecord, score: number): MedicationResultTone {
@@ -176,6 +228,7 @@ export function medicationResultTone(record: MedicationRecord, score: number): M
export function medicationToSearchResult(match: MedicationSearchMatch): MedicationSearchResult {
const { medication, score } = match;
+ const action = medicationActionDetail(medication);
return {
id: medication.slug,
name: medication.name,
@@ -183,7 +236,8 @@ export function medicationToSearchResult(match: MedicationSearchMatch): Medicati
match: score >= 12 ? "Exact clinical fit" : score >= 6 ? "Good clinical fit" : "Related match",
dose: medicationUsualDose(medication),
ceiling: medicationCeiling(medication),
- action: medicationAction(medication) ?? "Review full prescribing reference.",
+ action: action.text,
+ actionTone: action.tone,
tone: medicationResultTone(medication, score),
href: `/medications/${medication.slug}`,
};
diff --git a/tests/medications.test.ts b/tests/medications.test.ts
index a78790969..0ac77dcc2 100644
--- a/tests/medications.test.ts
+++ b/tests/medications.test.ts
@@ -1,7 +1,30 @@
import { describe, expect, it } from "vitest";
import { getMedicationRecord, loadMedicationSnapshot } from "@/lib/medication-snapshot";
-import { rankMedicationRecords } from "@/lib/medications";
+import {
+ firstClinicalSentence,
+ medicationActionDetail,
+ medicationToSearchResult,
+ rankMedicationRecords,
+ type MedicationRecord,
+} from "@/lib/medications";
+
+function buildRecord(overrides: Partial): MedicationRecord {
+ return {
+ slug: "test-med",
+ name: "Test Med",
+ class: "",
+ subclass: "",
+ category: "",
+ accent: "#0f766e",
+ tag: "",
+ schedule: "",
+ stats: [],
+ sections: [],
+ quick: [],
+ ...overrides,
+ };
+}
describe("medications catalogue", () => {
it("loads the full reviewed export snapshot", () => {
@@ -42,3 +65,147 @@ describe("medications catalogue", () => {
expect(record?.quick.length).toBeGreaterThan(0);
});
});
+
+describe("medication action tone", () => {
+ it("marks quick avoid guidance as danger", () => {
+ const record = buildRecord({
+ quick: [{ label: "Avoid if", value: "**Severe renal impairment.** Check creatinine first." }],
+ });
+ expect(medicationActionDetail(record)).toEqual({ text: "Severe renal impairment", tone: "danger" });
+ });
+
+ it("marks absolute contraindications as danger", () => {
+ const record = buildRecord({
+ sections: [
+ {
+ title: "Contraindications",
+ type: "contra",
+ rows: [{ key: "Absolute", val: "Known hypersensitivity. Do not rechallenge." }],
+ },
+ ],
+ });
+ expect(medicationActionDetail(record)).toEqual({ text: "Known hypersensitivity", tone: "danger" });
+ });
+
+ it("keeps summary clinical-focus text neutral even when monitoring rows exist", () => {
+ const record = buildRecord({
+ sections: [
+ {
+ title: "Summary",
+ type: "summary",
+ rows: [{ key: "Clinical focus", val: "Supports abstinence maintenance." }],
+ },
+ {
+ title: "Monitoring",
+ type: "mon",
+ rows: [{ key: "Laboratory", val: "Check LFTs at baseline." }],
+ },
+ ],
+ });
+ expect(medicationActionDetail(record)).toEqual({ text: "Supports abstinence maintenance", tone: "neutral" });
+ });
+
+ it("marks laboratory monitoring guidance as warning", () => {
+ const record = buildRecord({
+ sections: [
+ {
+ title: "Monitoring",
+ type: "mon",
+ rows: [{ key: "Laboratory", val: "Check LFTs at baseline and 3 months." }],
+ },
+ ],
+ });
+ expect(medicationActionDetail(record)).toEqual({
+ text: "Check LFTs at baseline and 3 months",
+ tone: "warning",
+ });
+ });
+
+ it("falls back to a neutral reference prompt", () => {
+ expect(medicationActionDetail(buildRecord({}))).toEqual({
+ text: "Review full prescribing reference",
+ tone: "neutral",
+ });
+ });
+
+ it("threads actionTone into search results", () => {
+ const record = buildRecord({
+ quick: [{ label: "Avoid if", value: "Severe renal impairment." }],
+ });
+ const result = medicationToSearchResult({ medication: record, score: 15, reasons: [] });
+ expect(result.actionTone).toBe("danger");
+ expect(result.action).toBe("Severe renal impairment");
+ });
+
+ it("flags acamprosate's renal contraindication as danger from the snapshot", () => {
+ const record = getMedicationRecord("acamprosate");
+ expect(record).toBeTruthy();
+ expect(medicationActionDetail(record as MedicationRecord).tone).toBe("danger");
+ });
+
+ it("does not mark explicit no-contraindication text as danger", () => {
+ const none = buildRecord({
+ quick: [{ label: "Avoid if", value: "NONE — No absolute contraindications in respiratory depression." }],
+ });
+ expect(medicationActionDetail(none).tone).toBe("neutral");
+
+ const noAbsolute = buildRecord({
+ quick: [{ label: "Avoid if", value: "No absolute contraindication in life-threatening anaphylaxis." }],
+ });
+ expect(medicationActionDetail(noAbsolute).tone).toBe("neutral");
+
+ const noneWithCaution = buildRecord({
+ quick: [{ label: "Avoid if", value: "None strictly absolute, but severe caution in respiratory failure." }],
+ });
+ expect(medicationActionDetail(noneWithCaution).tone).toBe("warning");
+ });
+
+ it("keeps naloxone's explicit NONE contraindication off the danger tone from the snapshot", () => {
+ const record = getMedicationRecord("naloxone");
+ expect(record).toBeTruthy();
+ expect(medicationActionDetail(record as MedicationRecord).tone).not.toBe("danger");
+ });
+
+ it("downgrades caution-only avoid guidance to warning", () => {
+ const pregnancyCategory = buildRecord({
+ quick: [{ label: "Avoid if", value: "Pregnancy Category B2." }],
+ });
+ expect(medicationActionDetail(pregnancyCategory).tone).toBe("warning");
+
+ const doseReview = buildRecord({
+ quick: [
+ { label: "Avoid if", value: "Liver disease requires pharmacist/doctor review and possible dose reduction." },
+ ],
+ });
+ expect(medicationActionDetail(doseReview).tone).toBe("warning");
+ });
+
+ it("keeps condition-list and setting-based hard stops on danger", () => {
+ const conditionList = buildRecord({
+ quick: [{ label: "Avoid if", value: "Severe respiratory depression, paralytic ileus." }],
+ });
+ expect(medicationActionDetail(conditionList).tone).toBe("danger");
+
+ const unmonitoredSetting = buildRecord({
+ quick: [{ label: "Avoid if", value: "Unmonitored environments without airway equipment." }],
+ });
+ expect(medicationActionDetail(unmonitoredSetting).tone).toBe("danger");
+ });
+
+ it("keeps fexofenadine and loratadine caution rows off the danger tone from the snapshot", () => {
+ for (const slug of ["fexofenadine", "loratadine"]) {
+ const record = getMedicationRecord(slug);
+ expect(record).toBeTruthy();
+ expect(medicationActionDetail(record as MedicationRecord).tone).toBe("warning");
+ }
+ });
+
+ it("does not cut action text at abbreviations or decimals", () => {
+ expect(firstClinicalSentence("Any hepatic impairment (e.g. cirrhosis). Review LFTs.")).toBe(
+ "Any hepatic impairment (e.g. cirrhosis)",
+ );
+ expect(firstClinicalSentence("Start 1.5 mg NOCTE. Titrate weekly.")).toBe("Start 1.5 mg NOCTE");
+ expect(firstClinicalSentence("Rash, nausea, etc. may occur. Stop if severe.")).toBe("Rash, nausea, etc. may occur");
+ expect(firstClinicalSentence("No trailing period")).toBe("No trailing period");
+ });
+});