From 648abfa3f7c91395b5eeca543f70e0b6ea59e9e0 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:54:23 +0800 Subject: [PATCH 1/6] docs: align readiness runtime version --- docs/production-readiness-checklist.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/production-readiness-checklist.md b/docs/production-readiness-checklist.md index e38e98c7f..1596dc639 100644 --- a/docs/production-readiness-checklist.md +++ b/docs/production-readiness-checklist.md @@ -2,9 +2,9 @@ This is the runbook to make the app publishable in one focused pass. -Last reviewed: 2026-07-04. Applies to any feature branch or release candidate. +Last reviewed: 2026-07-10. Applies to any feature branch or release candidate. -- Runtime target: Next.js 16.2.9, Node 24.x, npm 11.x. +- Runtime target: Next.js 16.2.10, Node 24.x, npm 11.x. - Supabase target: `sjrfecxgysukkwxsowpy` (`Clinical KB Database`). ## Immediate completion targets From 480ff2b7394186c223cc2f926b7adaf0e352b0b0 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 11 Jul 2026 03:09:27 +0800 Subject: [PATCH 2/6] feat: redesign medication search results with semantic badges and safety-aware actions - Derive actionTone (danger/warning/neutral) from the source field that supplies the prescribing action; render Ban/TriangleAlert icons with sr-only prefixes so contraindications carry visual + a11y weight - Surface per-row identity badges (S8, PBS, TGA, Reviewed) through the existing ClinicalBadge/medicationIdentityBadges system - Show the match-quality badge only when it differentiates rows - Add icons + live counts to the Best/Indication/Safety/Monitor chips; Safety/Monitor now also match on actionTone (regex missed "Contraindicated..." text) - Add empty states for zero-result filters (with reset) and no-match searches; gate the desktop table on non-empty results - Highlight the query token in medication names; micro-caps table header; hover chevron affordance; tabular numerals on doses - Add firstClinicalSentence so action/dose text no longer truncates at abbreviations like "e.g." or decimals Co-Authored-By: Claude Fable 5 --- .../medication-prescribing-workspace.tsx | 295 +++++++++++++----- .../use-medication-catalog.ts | 1 + src/lib/medications.ts | 59 +++- tests/medications.test.ts | 114 ++++++- 4 files changed, 377 insertions(+), 92 deletions(-) diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx index 35786ae0d..df5f84073 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,25 @@ 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 monitoring content), so it is a + // stronger signal than the text heuristics — a row whose action shows the danger + // icon must also be reachable through the Safety chip. + if (filter === "safety") { + return result.actionTone === "danger" || /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 +245,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 +280,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 ( - - + <> + {SEMANTIC_TONE_META[tone].ariaPrefix}: +