From 3f3c6e594dbff583f71ffe0aa45883252680ad2b Mon Sep 17 00:00:00 2001
From: Claude
{result.indication}
++ {result.indication} +
{showMatchBadge || result.match !== "Exact clinical fit" || row.badges.length > 0 ? (+
- {label} +
+ {metric.label}
- {value} -
- {meta ? ( -- {meta} -
- ) : null} +{metric.value}
{row.label}
+ ); + + if (value.length <= 110) { + return ( +{value}
++ {indication} +
+ ) : null}- {row.label} -
-- {row.value.replace(/\*\*/g, "")} -
-- {stat.label} -
-{stat.value}
-
diff --git a/src/lib/medications.ts b/src/lib/medications.ts
index 0912f91e0..9eea1c15f 100644
--- a/src/lib/medications.ts
+++ b/src/lib/medications.ts
@@ -152,16 +152,18 @@ export function medicationIndication(record: MedicationRecord) {
return firstClinicalSentence(raw.replace(/\*\*/g, "")) || raw;
}
-// Shared "short & sharp" clamp for constrained surfaces: take the first clinical
-// clause (protecting decimals/abbreviations, see firstClinicalSentence), strip
-// markdown bold, then cap on a word boundary with an ellipsis. Generalises the
-// slice(0, N)… idiom already used by formulationShortLabel (medication-badges.ts)
-// so every glanceable cell/tile trims content the same way. Full text is always
-// preserved in the deep-content sections/reference — this only shapes previews.
+// Shared "short & sharp" length cap for constrained surfaces (hero-metric tiles,
+// search cells): strip markdown bold, then cap on a word boundary with an
+// ellipsis. Generalises the slice(0, N)… idiom already used by
+// formulationShortLabel (medication-badges.ts). It intentionally does NOT split
+// on sentences — stat values are curated tokens that can carry dotted
+// abbreviations ("L.O.T. DRUG", "b.d.") — so callers that want the first clause
+// of prose compose firstClinicalSentence themselves (e.g. medicationIndication,
+// medicationUsualDose). Full text always remains in the sections/reference.
export function shortValue(text: string, cap = 24): string {
- const clause = firstClinicalSentence((text ?? "").replace(/\*\*/g, "").trim());
- if (clause.length <= cap) return clause;
- const slice = clause.slice(0, cap);
+ const cleaned = (text ?? "").replace(/\*\*/g, "").trim();
+ if (cleaned.length <= cap) return cleaned;
+ const slice = cleaned.slice(0, cap);
const boundary = slice.lastIndexOf(" ");
const head = (boundary > cap * 0.6 ? slice.slice(0, boundary) : slice).replace(/[\s,;:]+$/, "");
return `${head}…`;
@@ -326,11 +328,17 @@ export type MedicationHeroMetric = {
};
// "Max Dose" (and a bare "Dose") carry flag:"hi" to mark the prescribing
-// ceiling's importance, not a safety stop — render it as the primary/clinical
-// metric so red stays reserved for genuine risk signals (contraindications,
-// toxicity, teratogenicity), matching the #659 colour contract.
+// ceiling's importance, not a safety stop, so both tone and ordering treat it
+// specially — share one label test so the two stay aligned.
+function isMaxDoseLabel(label: string): boolean {
+ return /^(?:max\s+)?dose$/i.test(label.trim());
+}
+
+// Render Max Dose as the primary/clinical metric so red stays reserved for
+// genuine risk signals (contraindications, toxicity, teratogenicity), matching
+// the #659 colour contract.
function heroMetricTone(stat: MedicationStat): SemanticTone {
- if (/^(?:max\s+)?dose$/i.test(stat.label.trim())) return "clinical";
+ if (isMaxDoseLabel(stat.label)) return "clinical";
return medicationStatTone(stat);
}
@@ -343,8 +351,10 @@ function heroMetricTone(stat: MedicationStat): SemanticTone {
// sentences (indication/dosing/contraindications) are not tiled here — they live
// in the header subtitle and the detail sections instead.
export function medicationHeroMetrics(record: MedicationRecord): MedicationHeroMetric[] {
- const isMaxDose = (stat: MedicationStat) => /^(?:max\s+)?dose$/i.test(stat.label.trim());
- const ordered = [...record.stats.filter(isMaxDose), ...record.stats.filter((stat) => !isMaxDose(stat))];
+ const ordered = [
+ ...record.stats.filter((stat) => isMaxDoseLabel(stat.label)),
+ ...record.stats.filter((stat) => !isMaxDoseLabel(stat.label)),
+ ];
const metrics: MedicationHeroMetric[] = ordered.slice(0, 4).map((stat) => ({
label: stat.label,
value: shortValue(stat.value),
diff --git a/tests/medications.test.ts b/tests/medications.test.ts
index 4202f6e56..ba81d13a6 100644
--- a/tests/medications.test.ts
+++ b/tests/medications.test.ts
@@ -235,8 +235,11 @@ describe("shortValue", () => {
expect(out).not.toMatch(/[\s,;:]…$/); // no space/punctuation immediately before the ellipsis
});
- it("keeps only the first clinical clause, protecting decimals and abbreviations", () => {
- expect(shortValue("Start 1.5 mg NOCTE. Titrate weekly.", 40)).toBe("Start 1.5 mg NOCTE");
+ it("caps by length only — no sentence-splitting — so dotted abbreviations survive", () => {
+ // Stat values are curated tokens ("L.O.T. DRUG", "b.d."), not prose; splitting
+ // at an internal period would drop clinical label content.
+ expect(shortValue("L.O.T. DRUG")).toBe("L.O.T. DRUG");
+ expect(shortValue("Start 1.5 mg NOCTE. Titrate weekly.", 40)).toBe("Start 1.5 mg NOCTE. Titrate weekly.");
});
});
@@ -306,6 +309,15 @@ describe("medicationHeroMetrics", () => {
expect(metrics[0].label).toMatch(/max dose/i);
expect(metrics[0].tone).toBe("clinical");
});
+
+ it("preserves dotted-abbreviation stat values from the snapshot (L.O.T. DRUG)", () => {
+ const record = getMedicationRecord("lorazepam");
+ expect(record).toBeTruthy();
+ const hepaticSafe = medicationHeroMetrics(record as MedicationRecord).find(
+ (metric) => metric.label === "Hepatic Safe",
+ );
+ expect(hepaticSafe?.value).toBe("L.O.T. DRUG");
+ });
});
describe("medicationIndication", () => {
From efbda6f18f50daa6c6da7ff63cd2d291bc086359 Mon Sep 17 00:00:00 2001
From: Claude