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;