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 b033a034b..30f3ebe1e 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -161,8 +161,9 @@ 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-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)]"; } @@ -199,17 +200,99 @@ 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 +766,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-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; } 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;