From 8bccff162b14b4600205b571c5f1b57060defa28 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 8 Jul 2026 18:19:24 +0000 Subject: [PATCH 1/5] Fix differential badge design for mobile search results Rebased onto main and integrated with catalogue kind filters. - Redesign StatusBadge as pill with fixed height, solid emergent fill, and status dot to prevent text clipping on small screens - Replace cramped filter tab grid with scrollable flex row using split label/count pills, tablist semantics, 44px touch targets, and focus rings - Add narrow-viewport Playwright coverage for single-line tabs and badge Co-authored-by: BigSimmo --- .../clinical-dashboard/differentials-home.tsx | 133 ++++++++++++------ tests/ui-tools.spec.ts | 81 +++++++++++ 2 files changed, 173 insertions(+), 41 deletions(-) diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index b033a034b..1f267118a 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -161,8 +161,7 @@ function statusLabel(status: DifferentialRecord["status"]) { } function statusTone(status: DifferentialRecord["status"]) { - if (status === "emergent") - return "border-[color:var(--danger-border)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]"; + if (status === "emergent") return "border-transparent bg-[color:var(--danger)] text-white"; if (status === "urgent") { return "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]"; } @@ -199,17 +198,100 @@ function toDifferentialResult(item: DifferentialSearchResultItem): DifferentialR function StatusBadge({ status, className }: { status: DifferentialRecord["status"]; className?: string }) { return ( + {status === "emergent" ? ( + + ) : null} {statusLabel(status)} ); } +type KindFilter = "all" | "presentation" | "diagnosis"; + +const resultTypeTabFocusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +function ResultTypeTabs({ + activeFilter, + onFilterChange, + allCount, + presentationCount, + diagnosisCount, +}: { + activeFilter: KindFilter; + onFilterChange: (filter: KindFilter) => void; + allCount: number; + presentationCount: number; + diagnosisCount: number; +}) { + const tabs = [ + { id: "all" as const, label: "All", count: allCount }, + { id: "presentation" as const, label: "Presentations", count: presentationCount }, + { id: "diagnosis" as const, label: "Diagnoses", count: diagnosisCount }, + ]; + + return ( +
+ {tabs.map((tab) => { + const active = activeFilter === tab.id; + return ( + + ); + })} + +
+ ); +} + function MatchBadge({ label }: { label: string }) { const tone = label === "Best match" @@ -683,16 +765,6 @@ function SearchResultsView({ if (trimmedQuery && onRunSearch) onRunSearch(trimmedQuery); } - const kindFilterOptions = [ - { id: "all" as const, label: `All (${results.length})`, compact: "All" }, - { - id: "presentation" as const, - label: `Presentations (${presentationCount})`, - compact: `Pres (${presentationCount})`, - }, - { id: "diagnosis" as const, label: `Diagnoses (${diagnosisCount})`, compact: `Dx (${diagnosisCount})` }, - ]; - return (
toggleSelected(best.id)} /> -
- {kindFilterOptions.map((item) => ( - - ))} - -
+
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 1a99dc510..981a8410f 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -946,6 +946,87 @@ test.describe("Clinical KB tools launcher", () => { await expect(page.getByRole("link", { name: "Delirium / Acute Confusion / Encephalopathy" }).first()).toBeVisible(); }); + test("differentials search badges stay single-line on narrow viewport", async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + + await page.route(/\/api\/setup-status(?:\?.*)?$/, async (route) => { + await route.fulfill({ + json: { + demoMode: true, + checks: [ + { id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." }, + { id: "project", label: "[REDACTED] target", status: "ready", detail: "Test project ready." }, + { id: "schema", label: "supabase/schema.sql applied", status: "ready", detail: "Test schema ready." }, + { id: "search", label: "Search RPC and vector indexes", status: "ready", detail: "Test search ready." }, + { id: "openai", label: "OpenAI API key available", status: "ready", detail: "Test OpenAI ready." }, + ], + }, + }); + }); + await page.route(/\/api\/search(?:\?.*)?$/, async (route) => { + await route.fulfill({ + json: { + results: [], + visualEvidence: [], + relatedDocuments: [], + documentMatches: [ + { + document_id: "11111111-1111-4111-8111-111111111111", + title: "Acute confusion differential guide", + file_name: "acute-confusion-differentials.pdf", + labels: [], + summarySnippet: "Reviewed acute confusion differential guidance.", + bestPages: [1], + bestChunkIds: ["chunk-acute-confusion"], + imageCount: 0, + tableCount: 0, + matchReason: "Matched indexed passage", + score: 0.93, + }, + ], + relevance: { verdict: "strong", score: 0.91, directSourceCount: 1, weakSourceCount: 0 }, + smartPanel: {}, + telemetry: { query_class: "differential_compare", retrieval_strategy: "text_fast_path" }, + scope: { queryMode: "compare_guidance" }, + sourceGovernanceWarnings: [], + demoMode: true, + }, + }); + }); + + await gotoLauncher(page, "/differentials"); + await page.locator('input[placeholder="Ask or search a presentation"]:visible').first().fill("acute confusion"); + await page.locator('button[aria-label="Search differential presentations"]:visible').click(); + + await expect(page.getByTestId("differentials-search-results")).toBeVisible(); + const tabs = page.getByTestId("differential-result-type-tabs"); + await expect(tabs).toBeVisible(); + await expect(tabs.getByRole("tab", { name: /All \(\d+\)/ })).toBeVisible(); + await expect(tabs.getByRole("tab", { name: /Presentations \(\d+\)/ })).toBeVisible(); + await expect(tabs.getByRole("tab", { name: /Diagnoses \(\d+\)/ })).toBeVisible(); + + const tabMetrics = await tabs.getByRole("tab").evaluateAll((buttons) => + buttons.map((button) => { + const rect = button.getBoundingClientRect(); + return { height: rect.height, scrollHeight: button.scrollHeight }; + }), + ); + for (const tab of tabMetrics) { + expect(tab.scrollHeight).toBeLessThanOrEqual(tab.height + 1); + } + + const emergentBadge = page.getByTestId("differential-status-badge").first(); + await expect(emergentBadge).toBeVisible(); + await expect(emergentBadge).toHaveText(/Emergent/i); + const badgeMetrics = await emergentBadge.evaluate((element) => { + const rect = element.getBoundingClientRect(); + return { height: rect.height, scrollHeight: element.scrollHeight }; + }); + expect(badgeMetrics.height).toBeGreaterThanOrEqual(22); + expect(badgeMetrics.scrollHeight).toBeLessThanOrEqual(badgeMetrics.height + 1); + await expectNoPageHorizontalOverflow(page); + }); + test("differentials presentation comparison page stays wired to differentials mode", async ({ page }) => { await page.setViewportSize({ width: 1440, height: 920 }); const workflow = acuteConfusionPresentationWorkflow; From eb3c5c826f896e54593a0f1ce16c01ba89682ec8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 8 Jul 2026 18:21:03 +0000 Subject: [PATCH 2/5] fix(a11y): use aria-selected only on result type tabs Co-authored-by: BigSimmo --- src/components/clinical-dashboard/differentials-home.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index 1f267118a..525372a6e 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -252,7 +252,6 @@ function ResultTypeTabs({ type="button" role="tab" aria-selected={active} - aria-pressed={active} aria-label={`${tab.label} (${tab.count})`} onClick={() => onFilterChange(tab.id)} className={cn( From d578dddc6e51a4bcfe42e04d7c88ca6ff79164de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 9 Jul 2026 03:52:33 +0000 Subject: [PATCH 3/5] style: run Prettier on differentials-home for CI format:check Co-authored-by: BigSimmo --- src/components/clinical-dashboard/differentials-home.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index 525372a6e..f20242baf 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -205,9 +205,7 @@ function StatusBadge({ status, className }: { status: DifferentialRecord["status className, )} > - {status === "emergent" ? ( - - ) : null} + {status === "emergent" ? : null} {statusLabel(status)} ); From 19498d170a96966abf0bbf7c4998a7bced0e521b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 9 Jul 2026 04:09:03 +0000 Subject: [PATCH 4/5] fix(ui): use dark-mode-safe solid danger token for emergent badge Add --danger-solid and --danger-solid-contrast tokens so the emergent status pill keeps high contrast in dark mode instead of using --danger (which resolves to the pale text token as a background fill). Co-authored-by: BigSimmo --- src/app/globals.css | 6 ++++++ src/components/clinical-dashboard/differentials-home.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 346e83966..3bc755aef 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -192,6 +192,8 @@ --danger-text: #b42318; --danger-bg: #fef3f2; --danger-border: #fda29b; + --danger-solid: #b42318; + --danger-solid-contrast: #ffffff; --info: var(--info-text); --info-soft: var(--info-bg); @@ -355,6 +357,8 @@ --danger-text: #ff9ca4; --danger-bg: #401c24; --danger-border: #7f2e36; + --danger-solid: #b42318; + --danger-solid-contrast: #ffffff; --info: var(--info-text); --info-soft: var(--info-bg); @@ -2072,6 +2076,8 @@ summary::-webkit-details-marker { --warning-soft: Canvas; --danger: Mark; --danger-soft: Canvas; + --danger-solid: Mark; + --danger-solid-contrast: Canvas; --tone-purple: CanvasText; --tone-indigo: CanvasText; --tone-rose: CanvasText; diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index f20242baf..30f3ebe1e 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -161,7 +161,9 @@ function statusLabel(status: DifferentialRecord["status"]) { } function statusTone(status: DifferentialRecord["status"]) { - if (status === "emergent") return "border-transparent bg-[color:var(--danger)] text-white"; + if (status === "emergent") { + return "border-transparent bg-[color:var(--danger-solid)] text-[color:var(--danger-solid-contrast)]"; + } if (status === "urgent") { return "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]"; } @@ -205,7 +207,9 @@ function StatusBadge({ status, className }: { status: DifferentialRecord["status className, )} > - {status === "emergent" ? : null} + {status === "emergent" ? ( + + ) : null} {statusLabel(status)} ); From a695951577bae84afc93c8bc6f87ca145287e5fa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 9 Jul 2026 04:22:45 +0000 Subject: [PATCH 5/5] test(ui): harden mobile table fullscreen open against footer overlap races Wrap the accessible-table-surface click and dialog visibility assert in toPass so CI Chromium can retry when the composer footer briefly covers the tap target on 390px viewports. Co-authored-by: BigSimmo --- tests/ui-smoke.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 4b63cb98b..203ee8296 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -589,9 +589,11 @@ async function scrollMobileTableExpandClearOfFooter(page: Page, clinicalTable: L async function openMobileTableFullscreen(page: Page, clinicalTable: Locator) { await scrollMobileTableExpandClearOfFooter(page, clinicalTable); const tableSurface = clinicalTable.getByTestId("accessible-table-surface"); - await tableSurface.click({ force: true }); const tableDialog = page.getByTestId("table-fullscreen-dialog"); - await expect(tableDialog).toBeVisible({ timeout: 10_000 }); + await expect(async () => { + await tableSurface.click({ force: true }); + await expect(tableDialog).toBeVisible({ timeout: 2_000 }); + }).toPass({ timeout: 15_000 }); return tableDialog; }