From 43300cc7ad01f362a395487c4db2130bcca9b316 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:34:01 +0800 Subject: [PATCH 01/10] feat: add responsive tools results mockup --- docs/site-map.md | 1 + mockups/README.md | 4 + playwright.config.ts | 4 +- src/app/mockups/mockups-layout-client.tsx | 3 +- src/app/mockups/tools-search-mode/page.tsx | 12 + .../tools-search-mode-mockup.tsx | 458 ++++++++++++++++++ tests/ui-tools-search-mode-mockup.spec.ts | 133 +++++ 7 files changed, 612 insertions(+), 3 deletions(-) create mode 100644 src/app/mockups/tools-search-mode/page.tsx create mode 100644 src/components/tools-page-mockups/tools-search-mode-mockup.tsx create mode 100644 tests/ui-tools-search-mode-mockup.spec.ts diff --git a/docs/site-map.md b/docs/site-map.md index 8a332e752b..ed3010f6ba 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -1041,6 +1041,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/mockups/tools-action-workbench` - Route discovered from app directory Source: `src/app/mockups/tools-action-workbench/page.tsx`. - `/mockups/tools-clinical-lanes` - Route discovered from app directory Source: `src/app/mockups/tools-clinical-lanes/page.tsx`. - `/mockups/tools-command-center` - Route discovered from app directory Source: `src/app/mockups/tools-command-center/page.tsx`. +- `/mockups/tools-search-mode` - Route discovered from app directory Source: `src/app/mockups/tools-search-mode/page.tsx`. - `/mockups/tools-split-clinical-brief` - Route discovered from app directory Source: `src/app/mockups/tools-split-clinical-brief/page.tsx`. - `/mockups/tools-split-compact-sheet` - Route discovered from app directory Source: `src/app/mockups/tools-split-compact-sheet/page.tsx`. - `/mockups/tools-split-pane` - Route discovered from app directory Source: `src/app/mockups/tools-split-pane/page.tsx`. diff --git a/mockups/README.md b/mockups/README.md index a06679be58..e0eadc796a 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -61,6 +61,10 @@ These are PNGs for design review only. Runnable `/mockups/*` routes are a separa **Perfected combined comps** (desktop + phone in one image, recommended directions only) live in [`public/mockups/mode-page-redesign-2026-07/perfected-combined/`](../public/mockups/mode-page-redesign-2026-07/perfected-combined/README.md). +## Perfected Tools search mode + +`/mockups/tools-search-mode?mode=tools&q=Compare` is the interactive responsive Tools results-mode study. It uses the site's universal header and search composer, the shared results ribbon and filter conventions, compact Tools result cards, a contextual detail panel on desktop, and a bottom sheet on phones. It intentionally excludes the Tools home hero, quick actions, medication-list treatment, and cross-mode suggestion blocks. + ## Breadcrumb header study (2026-08-09) — shipped, route retired Three sticky header directions for record pages that use `InformationPageBreadcrumbs` and have **no in-page section index** — factsheets, services, forms, DSM, specifiers, formulation, medications. `InPageNavHeader` stays the default for in-page navigation per [`docs/search-chrome-behaviour.md`](../docs/search-chrome-behaviour.md); it is the wrong shape for those pages, because with no sections its disclosure opens a one-item sheet and its weighted track renders one full-width segment. Every direction kept that header's row grammar (back, title, ellipsis, one scroll owner) and dropped the section machinery: diff --git a/playwright.config.ts b/playwright.config.ts index ae292f68b0..b9a5bb7e6f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,13 +25,13 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; const productionSpecPattern = /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|document-canvas|tools|overlap|universal-search|specifiers|formulation|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/; const mockupSpecPattern = - /.*ui-(document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-task-directory)\.spec\.ts/; + /.*ui-(document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory)\.spec\.ts/; const mockupTag = /@mockup/; export default defineConfig({ testDir: "./tests", testMatch: - /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|document-canvas|document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/, + /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|document-canvas|document-top-navigation-mockup|therapy-navigation-mockup|tools|tools-collapse|tools-search-mode-mockup|tools-task-directory|overlap|universal-search|specifiers|formulation|forms-section-nav|chrome-scroll|therapy-nav-scroll|mode-nav-density|phone-scroll(?:-[a-z0-9-]+)?|pwa|route-coverage|style-contract|visual-artifacts|hydration))\.spec\.ts/, timeout: 60_000, retries: 0, // Fail the run if a stray `test.only` is committed: otherwise it silently diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index 6a2af37cd5..96acd99841 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -8,6 +8,7 @@ import { GlobalMockupSearchShell } from "@/components/clinical-dashboard/global- export function MockupsLayoutClient({ children }: { children: ReactNode }) { const pathname = usePathname(); const isToolsPageMockup = pathname.startsWith("/mockups/tools-"); + const isToolsSearchModeMockup = pathname === "/mockups/tools-search-mode"; const isFavouritesPageMockup = pathname.startsWith("/mockups/favourites-"); const isDocumentSearchMockup = pathname.startsWith("/mockups/document-search"); const isDocumentTopNavigationMockup = pathname === "/mockups/document-top-navigation"; @@ -81,7 +82,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { : "answer" } searchComposerVisible={ - !isToolsPageMockup && + (!isToolsPageMockup || isToolsSearchModeMockup) && !isFavouritesPageMockup && !isStandaloneDocumentFlow && !isDocumentTopNavigationMockup && diff --git a/src/app/mockups/tools-search-mode/page.tsx b/src/app/mockups/tools-search-mode/page.tsx new file mode 100644 index 0000000000..bd1f144a97 --- /dev/null +++ b/src/app/mockups/tools-search-mode/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { ToolsSearchModeMockup } from "@/components/tools-page-mockups/tools-search-mode-mockup"; + +export const metadata: Metadata = { + title: "Perfected Tools Results Mode Mockup - Clinical KB", + description: "Responsive Tools results page with the universal search composer and adaptive tool details.", +}; + +export default function ToolsSearchModeMockupRoute() { + return ; +} diff --git a/src/components/tools-page-mockups/tools-search-mode-mockup.tsx b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx new file mode 100644 index 0000000000..e218729144 --- /dev/null +++ b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx @@ -0,0 +1,458 @@ +"use client"; + +import Link from "next/link"; +import { + Brain, + ChevronRight, + ClipboardList, + FileCheck2, + FileText, + Grid2X2, + Palette, + Pill, + Search, + ShieldCheck, + Users, + Waves, + type LucideIcon, +} from "lucide-react"; +import { type ReactNode, useId, useMemo, useRef, useState } from "react"; + +import { + ResultFilterSheet, + ResultFilterTrigger, + resultFilterGroup, +} from "@/components/clinical-dashboard/result-filter-control"; +import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context"; +import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search-results-header-band"; +import { cn, controlBase, floatingControl } from "@/components/ui-primitives"; +import { Sheet } from "@/components/ui/sheet"; +import { toolCatalogRecords, toolSearchText, type ToolCatalogArea, type ToolCatalogRecord } from "@/lib/tools-catalog"; + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +const iconByToolId: Record = { + "clinical-kb-search": Search, + differentials: Brain, + "medication-prescribing": Pill, + "risk-safety": ShieldCheck, + documents: FileText, + services: Users, + forms: FileCheck2, +}; + +const filterOptions = [ + { id: "all", label: "All tools" }, + { id: "assessment", label: "Assessment" }, + { id: "reference", label: "Evidence" }, + { id: "care", label: "Treatment" }, + { id: "coordination", label: "Coordination" }, +] as const satisfies ReadonlyArray<{ id: "all" | ToolCatalogArea; label: string }>; + +type FilterId = (typeof filterOptions)[number]["id"]; +type DetailSectionId = "check-first" | "needed-input" | "output"; + +function ToolIcon({ tool, large = false }: { tool: ToolCatalogRecord; large?: boolean }) { + const Icon = iconByToolId[tool.id] ?? Grid2X2; + return ( + + + ); +} + +function ToolChips({ tool }: { tool: ToolCatalogRecord }) { + return ( + + {tool.sourceBacked ? ( + + + ) : null} + {tool.highYield ? ( + + High yield + + ) : null} + + ); +} + +function DetailAccordion({ + tool, + prefix, + openSection, + onToggle, +}: { + tool: ToolCatalogRecord; + prefix: string; + openSection: DetailSectionId | null; + onToggle: (section: DetailSectionId) => void; +}) { + const sections: Array<{ id: DetailSectionId; label: string; icon: LucideIcon; content: ReactNode }> = [ + { + id: "check-first", + label: "Check first", + icon: ShieldCheck, + content: ( + + ), + }, + { + id: "needed-input", + label: "Needed input", + icon: ClipboardList, + content: ( + + ), + }, + { id: "output", label: "Output", icon: Waves, content:

{tool.output}

}, + ]; + + return ( +
+ {sections.map(({ id, label, icon: Icon, content }) => { + const expanded = openSection === id; + const panelId = `${prefix}-${id}`; + return ( +
+

+ +

+ +
+ ); + })} +
+ ); +} + +function DetailBody({ + tool, + prefix, + openSection, + onToggle, +}: { + tool: ToolCatalogRecord; + prefix: string; + openSection: DetailSectionId | null; + onToggle: (section: DetailSectionId) => void; +}) { + return ( +
+
+
+
+

{tool.detail}

+
+ +
+ ); +} + +function DetailActions({ tool }: { tool: ToolCatalogRecord }) { + return ( +
+ + {tool.actionLabel} {tool.title} +
+ ); +} + +export function ToolsSearchModeMockup() { + const searchCommand = useSearchCommand(); + const query = searchCommand?.query ?? "Compare"; + const filterPanelId = useId(); + const [activeFilter, setActiveFilter] = useState("all"); + const [filterOpen, setFilterOpen] = useState(false); + const [selectedId, setSelectedId] = useState("differentials"); + const [phoneDetailOpen, setPhoneDetailOpen] = useState(false); + const [openSection, setOpenSection] = useState(null); + const detailReturnFocusRef = useRef(null); + const desktopPanelRef = useRef(null); + + const queryMatchedTools = useMemo(() => { + const normalized = query.trim().toLowerCase(); + return toolCatalogRecords.filter( + (tool) => (!normalized || toolSearchText(tool).includes(normalized)) && tool.id !== "favourites", + ); + }, [query]); + + const filterCounts = useMemo>( + () => ({ + all: queryMatchedTools.length, + assessment: queryMatchedTools.filter((tool) => tool.area === "assessment").length, + reference: queryMatchedTools.filter((tool) => tool.area === "reference").length, + care: queryMatchedTools.filter((tool) => tool.area === "care").length, + coordination: queryMatchedTools.filter((tool) => tool.area === "coordination").length, + }), + [queryMatchedTools], + ); + + const filteredTools = useMemo( + () => (activeFilter === "all" ? queryMatchedTools : queryMatchedTools.filter((tool) => tool.area === activeFilter)), + [activeFilter, queryMatchedTools], + ); + + const selectedTool = filteredTools.find((tool) => tool.id === selectedId) ?? filteredTools[0] ?? null; + + function toggleDetailSection(section: DetailSectionId) { + setOpenSection((current) => (current === section ? null : section)); + } + + function openTool(tool: ToolCatalogRecord, opener?: HTMLElement | null) { + setSelectedId(tool.id); + setOpenSection(null); + if (window.matchMedia("(max-width: 1023px)").matches) { + detailReturnFocusRef.current = opener ?? null; + setPhoneDetailOpen(true); + return; + } + window.requestAnimationFrame(() => desktopPanelRef.current?.focus()); + } + + return ( +
+
+
+ setFilterOpen((current) => !current)} + title="Filter tools" + /> + } + mobileControlsPlacement="inline" + filterControls={ + setFilterOpen((current) => !current)} + title="Filter tools" + /> + } + /> + setFilterOpen(false)} + panelId={filterPanelId} + testId="tools-search-filter-sheet" + title="Filter tools" + groups={[ + resultFilterGroup({ + id: "category", + label: "Category", + value: activeFilter, + options: filterOptions.map((option) => ({ + value: option.id, + label: option.label, + hint: String(filterCounts[option.id]), + })), + onChange: setActiveFilter, + }), + ]} + onClearAll={activeFilter === "all" ? undefined : () => setActiveFilter("all")} + footerNote={`${filteredTools.length} showing`} + /> + +
+ {filteredTools.length ? ( + filteredTools.slice(0, 4).map((tool) => ( +
+ +
+

{tool.title}

+

{tool.description}

+ + + +
+ +
+ )) + ) : ( +
+
+ )} +
+
+ + {selectedTool ? ( + + ) : null} +
+ +
+ +
+ + {selectedTool ? ( +
+ setPhoneDetailOpen(false)} + title={selectedTool.title} + closeLabel={`Close ${selectedTool.title}`} + headerLeading={} + descriptionContent={} + titleClassName="text-xl font-extrabold" + contentClassName="sm:max-w-[39rem]" + bodyClassName="grid gap-4" + portal={false} + returnFocusRef={detailReturnFocusRef} + testId="tools-search-detail-sheet" + footer={} + > + + +
+ ) : null} +
+ ); +} diff --git a/tests/ui-tools-search-mode-mockup.spec.ts b/tests/ui-tools-search-mode-mockup.spec.ts new file mode 100644 index 0000000000..47c69371db --- /dev/null +++ b/tests/ui-tools-search-mode-mockup.spec.ts @@ -0,0 +1,133 @@ +import { expect, test, type Page } from "playwright/test"; + +const mockupPath = "/mockups/tools-search-mode?mode=tools&q=Compare"; + +async function gotoMockup(page: Page, width: number, height = 900) { + await page.setViewportSize({ width, height }); + await page.goto(mockupPath, { waitUntil: "domcontentloaded" }); + const mockup = page.locator('[data-testid="tools-search-mode-mockup"]:visible'); + await expect(mockup).toBeVisible(); + await expect(page.locator('[data-testid="global-search-input"]:visible')).toHaveValue("Compare"); + return mockup; +} + +async function expectNoHorizontalOverflow(page: Page) { + const geometry = await page.evaluate(() => ({ + body: document.body.scrollWidth, + document: document.documentElement.scrollWidth, + viewport: document.documentElement.clientWidth, + })); + expect(Math.max(geometry.body, geometry.document)).toBeLessThanOrEqual(geometry.viewport + 1); +} + +test.describe("Perfected Tools results mode mockup @mockup", () => { + test("desktop uses universal search and keeps results beside the selected-tool panel", async ({ page }) => { + const mockup = await gotoMockup(page, 1440); + + await expect(mockup.getByRole("heading", { level: 1, name: "Compare" })).toBeVisible(); + await expect(mockup.getByText("1 tool", { exact: true })).toBeVisible(); + await expect(mockup.getByRole("heading", { level: 2, name: "Differentials" }).first()).toBeVisible(); + await expect(mockup.locator("aside").getByRole("heading", { name: "Best for" })).toBeVisible(); + await expect(mockup.getByTestId("tools-search-mode-hero")).toHaveCount(0); + await expect(mockup.getByText("Also matches in other modes")).toHaveCount(0); + await expect(mockup.getByText("Dose converter")).toHaveCount(0); + + await mockup.getByTestId("tools-search-filter-trigger-wide").click(); + const filterSheet = page.locator('[data-testid="tools-search-filter-sheet"]:visible'); + await filterSheet.getByRole("radio", { name: /Treatment/ }).click(); + await filterSheet.getByRole("button", { name: "Done" }).click(); + await expect(mockup.getByText("0 tools", { exact: true })).toBeVisible(); + await expect(mockup.getByRole("heading", { name: "No tools match" })).toBeVisible(); + await expect(mockup.locator("aside")).toHaveCount(0); + await mockup.getByTestId("tools-search-filter-trigger-wide").click(); + await filterSheet.getByRole("radio", { name: /All tools/ }).click(); + await filterSheet.getByRole("button", { name: "Done" }).click(); + await expect(mockup.getByRole("heading", { level: 2, name: "Differentials" }).first()).toBeVisible(); + await expect(mockup.locator("aside").getByRole("heading", { name: "Differentials" })).toBeVisible(); + + const output = mockup.locator("aside").getByRole("button", { name: "Output" }); + await output.click(); + await expect(output).toHaveAttribute("aria-expanded", "true"); + await expect(mockup.locator("#desktop-tool-detail-output")).toContainText("Ranked differentials"); + + await page.locator('[data-testid="global-search-input"]:visible').fill("Safety"); + await expect(mockup.getByRole("heading", { level: 1, name: "Safety" })).toBeVisible(); + await expect(mockup.locator("aside").getByRole("heading", { name: "Risk & Safety" })).toBeVisible(); + await expectNoHorizontalOverflow(page); + }); + + test("phone keeps results visible until Details opens the preferred bottom sheet", async ({ page }) => { + const mockup = await gotoMockup(page, 390, 844); + + const sheet = page.locator('[data-testid="tools-search-detail-sheet"]:visible'); + await expect(sheet).toHaveCount(0); + await expect(mockup.getByRole("heading", { level: 1, name: "Compare" })).toBeVisible(); + await expect(mockup.getByRole("heading", { level: 2, name: "Differentials" })).toBeVisible(); + + const details = mockup.getByRole("button", { name: "View details for Differentials" }); + await details.click(); + await expect(sheet).toBeVisible(); + await expect(sheet.getByRole("heading", { name: "Differentials" })).toBeVisible(); + await expect(sheet.getByRole("heading", { name: "Best for" })).toBeVisible(); + + const neededInput = sheet.getByRole("button", { name: "Needed input" }); + await neededInput.click(); + await expect(neededInput).toHaveAttribute("aria-expanded", "true"); + await page.keyboard.press("Escape"); + await expect(sheet).toHaveCount(0); + + await details.click(); + await expect(sheet).toBeVisible(); + await sheet.getByRole("button", { name: "Close Differentials" }).click(); + await expect(details).toBeFocused(); + await expectNoHorizontalOverflow(page); + }); + + test("phone filter sheet follows the shared local-filter behavior", async ({ page }) => { + const mockup = await gotoMockup(page, 390, 844); + const trigger = mockup.getByTestId("tools-search-filter-trigger-phone"); + + await trigger.click(); + const filterSheet = page.locator('[data-testid="tools-search-filter-sheet"]:visible'); + await expect(filterSheet).toBeVisible(); + await expect(filterSheet.getByText("1 showing", { exact: true })).toBeVisible(); + + const assessment = filterSheet.getByRole("radio", { name: /Assessment/ }); + await assessment.click(); + await expect(assessment).toHaveAttribute("aria-checked", "true"); + await expect(filterSheet).toBeVisible(); + await expect(filterSheet.getByText("1 showing", { exact: true })).toBeVisible(); + await filterSheet.getByRole("button", { name: "Done" }).click(); + await expect(filterSheet).toHaveCount(0); + await expect(trigger).toContainText("1"); + + await trigger.click(); + const treatment = filterSheet.getByRole("radio", { name: /Treatment/ }); + await treatment.click(); + await expect(treatment).toHaveAttribute("aria-checked", "true"); + await expect(filterSheet.getByText("0 showing", { exact: true })).toBeVisible(); + await filterSheet.getByRole("button", { name: "Clear filters" }).click(); + await expect(filterSheet).toBeVisible(); + await expect(filterSheet.getByRole("radio", { name: /All tools/ })).toHaveAttribute("aria-checked", "true"); + await expect(filterSheet.getByText("1 showing", { exact: true })).toBeVisible(); + await expectNoHorizontalOverflow(page); + }); + + test("keeps its shared controls and tool sheet legible in forced colors with reduced motion", async ({ page }) => { + await page.emulateMedia({ forcedColors: "active", reducedMotion: "reduce" }); + const mockup = await gotoMockup(page, 390, 844); + + await mockup.getByRole("button", { name: "View details for Differentials" }).click(); + const sheet = page.locator('[data-testid="tools-search-detail-sheet"]:visible'); + await expect(sheet.getByText("Source-backed")).toBeVisible(); + await expect(sheet.getByRole("link", { name: "Compare Differentials" })).toBeVisible(); + await expectNoHorizontalOverflow(page); + }); + + for (const width of [320, 390, 639, 768, 1440, 1920]) { + test(`has no horizontal overflow at ${width}px`, async ({ page }) => { + await gotoMockup(page, width, 900); + await expectNoHorizontalOverflow(page); + }); + } +}); From 1e95c901b7397101b730e96a032b47a706a60301 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:47:40 +0800 Subject: [PATCH 02/10] fix: align tools filters with shared controls --- .../tools-search-mode-mockup.tsx | 30 ++++++++------ tests/ui-tools-search-mode-mockup.spec.ts | 41 +++++++++++-------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/components/tools-page-mockups/tools-search-mode-mockup.tsx b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx index e218729144..d063253fd5 100644 --- a/src/components/tools-page-mockups/tools-search-mode-mockup.tsx +++ b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx @@ -26,6 +26,7 @@ import { import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context"; import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search-results-header-band"; import { cn, controlBase, floatingControl } from "@/components/ui-primitives"; +import { SegmentedControl } from "@/components/ui/segmented-control"; import { Sheet } from "@/components/ui/sheet"; import { toolCatalogRecords, toolSearchText, type ToolCatalogArea, type ToolCatalogRecord } from "@/lib/tools-catalog"; @@ -254,6 +255,17 @@ export function ToolsSearchModeMockup() { [activeFilter, queryMatchedTools], ); + const filterControlOptions = useMemo( + () => + filterOptions.map((option) => ({ + value: option.id, + label: option.label, + hint: String(filterCounts[option.id]), + disabled: filterCounts[option.id] === 0 && activeFilter !== option.id, + })), + [activeFilter, filterCounts], + ); + const selectedTool = filteredTools.find((tool) => tool.id === selectedId) ?? filteredTools[0] ?? null; function toggleDetailSection(section: DetailSectionId) { @@ -301,13 +313,11 @@ export function ToolsSearchModeMockup() { } mobileControlsPlacement="inline" filterControls={ - setFilterOpen((current) => !current)} - title="Filter tools" + } /> @@ -322,11 +332,7 @@ export function ToolsSearchModeMockup() { id: "category", label: "Category", value: activeFilter, - options: filterOptions.map((option) => ({ - value: option.id, - label: option.label, - hint: String(filterCounts[option.id]), - })), + options: filterControlOptions, onChange: setActiveFilter, }), ]} diff --git a/tests/ui-tools-search-mode-mockup.spec.ts b/tests/ui-tools-search-mode-mockup.spec.ts index 47c69371db..48bd90ce54 100644 --- a/tests/ui-tools-search-mode-mockup.spec.ts +++ b/tests/ui-tools-search-mode-mockup.spec.ts @@ -32,16 +32,23 @@ test.describe("Perfected Tools results mode mockup @mockup", () => { await expect(mockup.getByText("Also matches in other modes")).toHaveCount(0); await expect(mockup.getByText("Dose converter")).toHaveCount(0); - await mockup.getByTestId("tools-search-filter-trigger-wide").click(); - const filterSheet = page.locator('[data-testid="tools-search-filter-sheet"]:visible'); - await filterSheet.getByRole("radio", { name: /Treatment/ }).click(); - await filterSheet.getByRole("button", { name: "Done" }).click(); + const categoryRail = mockup.getByRole("radiogroup", { name: "Tool category" }); + const allToolsFilter = categoryRail.getByRole("radio", { name: "All tools (1)" }); + await allToolsFilter.focus(); + await page.keyboard.press("ArrowRight"); + await expect(categoryRail.getByRole("radio", { name: "Assessment (1)" })).toHaveAttribute("aria-checked", "true"); + await expect(categoryRail.getByRole("radio", { name: "Treatment (0)" })).toBeDisabled(); + + await page.locator('[data-testid="global-search-input"]:visible').fill("Safety"); + await expect(mockup.getByRole("heading", { level: 1, name: "Safety" })).toBeVisible(); + const treatmentFilter = categoryRail.getByRole("radio", { name: "Treatment (2)" }); + await treatmentFilter.click(); + await expect(treatmentFilter).toHaveAttribute("aria-checked", "true"); + await page.locator('[data-testid="global-search-input"]:visible').fill("Compare"); await expect(mockup.getByText("0 tools", { exact: true })).toBeVisible(); await expect(mockup.getByRole("heading", { name: "No tools match" })).toBeVisible(); await expect(mockup.locator("aside")).toHaveCount(0); - await mockup.getByTestId("tools-search-filter-trigger-wide").click(); - await filterSheet.getByRole("radio", { name: /All tools/ }).click(); - await filterSheet.getByRole("button", { name: "Done" }).click(); + await allToolsFilter.click(); await expect(mockup.getByRole("heading", { level: 2, name: "Differentials" }).first()).toBeVisible(); await expect(mockup.locator("aside").getByRole("heading", { name: "Differentials" })).toBeVisible(); @@ -87,29 +94,29 @@ test.describe("Perfected Tools results mode mockup @mockup", () => { const mockup = await gotoMockup(page, 390, 844); const trigger = mockup.getByTestId("tools-search-filter-trigger-phone"); + await page.locator('[data-testid="global-search-input"]:visible').fill("Safety"); + await expect(mockup.getByRole("heading", { level: 1, name: "Safety" })).toBeVisible(); + await trigger.click(); const filterSheet = page.locator('[data-testid="tools-search-filter-sheet"]:visible'); await expect(filterSheet).toBeVisible(); - await expect(filterSheet.getByText("1 showing", { exact: true })).toBeVisible(); + await expect(filterSheet.getByText("2 showing", { exact: true })).toBeVisible(); + await expect(filterSheet.getByRole("radio", { name: /Evidence/ })).toHaveAttribute("aria-disabled", "true"); - const assessment = filterSheet.getByRole("radio", { name: /Assessment/ }); - await assessment.click(); - await expect(assessment).toHaveAttribute("aria-checked", "true"); + const treatment = filterSheet.getByRole("radio", { name: /Treatment/ }); + await treatment.click(); + await expect(treatment).toHaveAttribute("aria-checked", "true"); await expect(filterSheet).toBeVisible(); - await expect(filterSheet.getByText("1 showing", { exact: true })).toBeVisible(); + await expect(filterSheet.getByText("2 showing", { exact: true })).toBeVisible(); await filterSheet.getByRole("button", { name: "Done" }).click(); await expect(filterSheet).toHaveCount(0); await expect(trigger).toContainText("1"); await trigger.click(); - const treatment = filterSheet.getByRole("radio", { name: /Treatment/ }); - await treatment.click(); - await expect(treatment).toHaveAttribute("aria-checked", "true"); - await expect(filterSheet.getByText("0 showing", { exact: true })).toBeVisible(); await filterSheet.getByRole("button", { name: "Clear filters" }).click(); await expect(filterSheet).toBeVisible(); await expect(filterSheet.getByRole("radio", { name: /All tools/ })).toHaveAttribute("aria-checked", "true"); - await expect(filterSheet.getByText("1 showing", { exact: true })).toBeVisible(); + await expect(filterSheet.getByText("2 showing", { exact: true })).toBeVisible(); await expectNoHorizontalOverflow(page); }); From 71fd1b3fb730c6231db2ae9d4dd52728b4a0a67f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 13 Aug 2026 02:11:18 +0800 Subject: [PATCH 03/10] test: cover tools results review regressions --- tests/ui-tools-search-mode-mockup.spec.ts | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/ui-tools-search-mode-mockup.spec.ts b/tests/ui-tools-search-mode-mockup.spec.ts index 48bd90ce54..8e8d71782f 100644 --- a/tests/ui-tools-search-mode-mockup.spec.ts +++ b/tests/ui-tools-search-mode-mockup.spec.ts @@ -63,6 +63,22 @@ test.describe("Perfected Tools results mode mockup @mockup", () => { await expectNoHorizontalOverflow(page); }); + test("matches the exact displayed tool title after normalising punctuation", async ({ page }) => { + const mockup = await gotoMockup(page, 1440); + await page.locator('[data-testid="global-search-input"]:visible').fill("Risk & Safety"); + + await expect(mockup.getByText("1 tool", { exact: true })).toBeVisible(); + await expect(mockup.getByRole("heading", { level: 2, name: "Risk & Safety" }).first()).toBeVisible(); + }); + + test("renders every result included in the reported count", async ({ page }) => { + const mockup = await gotoMockup(page, 1440); + await page.locator('[data-testid="global-search-input"]:visible').fill(""); + + await expect(mockup.getByText("12 tools", { exact: true })).toBeVisible(); + await expect(mockup.locator('section[aria-label="Tool results"] article')).toHaveCount(12); + }); + test("phone keeps results visible until Details opens the preferred bottom sheet", async ({ page }) => { const mockup = await gotoMockup(page, 390, 844); @@ -90,6 +106,21 @@ test.describe("Perfected Tools results mode mockup @mockup", () => { await expectNoHorizontalOverflow(page); }); + test("closes the phone detail sheet when the viewport enters desktop layout", async ({ page }) => { + const mockup = await gotoMockup(page, 390, 844); + const sheet = page.locator('[data-testid="tools-search-detail-sheet"]:visible'); + + await mockup.getByRole("button", { name: "View details for Differentials" }).click(); + await expect(sheet).toBeVisible(); + + await page.setViewportSize({ width: 1440, height: 900 }); + await expect(sheet).toHaveCount(0); + await expect(mockup.locator("aside")).toBeVisible(); + + await page.setViewportSize({ width: 390, height: 844 }); + await expect(sheet).toHaveCount(0); + }); + test("phone filter sheet follows the shared local-filter behavior", async ({ page }) => { const mockup = await gotoMockup(page, 390, 844); const trigger = mockup.getByTestId("tools-search-filter-trigger-phone"); From 987918e609ffe571f631a22e75df0bf11c4fc898 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 13 Aug 2026 02:41:10 +0800 Subject: [PATCH 04/10] style: restore formatted file ending --- src/components/tools-page-mockups/tools-search-mode-mockup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tools-page-mockups/tools-search-mode-mockup.tsx b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx index ceddde7e33..8463772291 100644 --- a/src/components/tools-page-mockups/tools-search-mode-mockup.tsx +++ b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx @@ -472,4 +472,4 @@ export function ToolsSearchModeMockup() { ) : null} ); -} \ No newline at end of file +} From d548f6bf619e4d965b7567002f77a9d484a82963 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:47:58 +0800 Subject: [PATCH 05/10] style: refine tools results mockup --- .../tools-search-mode-mockup.tsx | 29 +++++++++++++++---- tests/ui-tools-search-mode-mockup.spec.ts | 12 ++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/components/tools-page-mockups/tools-search-mode-mockup.tsx b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx index d063253fd5..8057e3892c 100644 --- a/src/components/tools-page-mockups/tools-search-mode-mockup.tsx +++ b/src/components/tools-page-mockups/tools-search-mode-mockup.tsx @@ -342,30 +342,44 @@ export function ToolsSearchModeMockup() {
{filteredTools.length ? ( - filteredTools.slice(0, 4).map((tool) => ( + filteredTools.map((tool) => (
+ {tool.id === selectedTool?.id ? ( +