From d9aba577bbc4d16d5ae481d3091399c72dc6bc18 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 06:33:32 +0000 Subject: [PATCH 1/6] Unify information-page chrome across catalogue modes. Add a shared InformationPageShell and route helper, then adopt them on services, forms, medications, factsheets, DSM, and the existing specifier/formulation shells so detail pages share landmark, padding, and breadcrumb structure. Co-authored-by: BigSimmo --- docs/codebase-index.md | 1 + .../global-search-shell.tsx | 60 +- .../medication-record-page.tsx | 45 +- src/components/dsm/dsm-diagnosis-page.tsx | 5 +- src/components/dsm/dsm-page-header.tsx | 17 +- .../factsheets/factsheet-detail-page.tsx | 17 +- src/components/forms/form-detail-page.tsx | 572 +++++++++--------- src/components/formulation/formulation-ui.tsx | 41 +- src/components/information-page-shell.tsx | 177 ++++++ .../services/service-detail-page.tsx | 439 +++++++------- src/components/specifiers/specifier-ui.tsx | 51 +- src/lib/information-pages.ts | 78 +++ tests/forms-back-navigation.dom.test.tsx | 31 +- tests/information-page-shell.test.tsx | 56 ++ tests/ui-smoke.spec.ts | 2 +- tests/ui-tools.spec.ts | 4 +- 16 files changed, 848 insertions(+), 748 deletions(-) create mode 100644 src/components/information-page-shell.tsx create mode 100644 src/lib/information-pages.ts create mode 100644 tests/information-page-shell.test.tsx diff --git a/docs/codebase-index.md b/docs/codebase-index.md index aa92ad7ad..f5ccf5291 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -301,6 +301,7 @@ sequenceDiagram One shared composer (`master-search-header.tsx`) serves every mode. Placement: - **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/formulation`, `/tools`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike. (`/applications` is a redirect to `/tools`, not a composer surface.) +- **Information (detail) pages**: catalogue/record routes under each mode (`/services/[slug]`, `/forms/[slug]`, `/medications/[slug]`, `/specifiers/[slug]`, `/formulation/[slug]`, `/factsheets/[slug]`, `/dsm/diagnoses/[slug]`, …). Route detection: `src/lib/information-pages.ts` (`isInformationPage`). Shared outer chrome: `src/components/information-page-shell.tsx` (`InformationPageShell`, breadcrumbs, optional footer). Specifier/formulation mode shells re-export that primitive. Intentional opt-outs: document viewer, therapy-compass CSS workspace, differentials presentation workflow. - **Result and detail views**: fixed bottom dock on phone (compact variant on submitted searches), sticky top from `sm` up. - **Results routing**: standalone routes own their submitted searches via `?q=…&run=1` (`/services` → `ServicesNavigatorPage`, `/forms` → `FormsSearchResultsPage`, `/differentials` → `DifferentialsHome` results view, `/formulation` → local mechanism results, `/favourites` filters the command library in place). Answer, Documents, and Prescribing submitted searches render inside `ClinicalDashboard` — intentional, since they need retrieval/answer state. `/?mode=favourites` redirects to `/favourites`; `/?mode=differentials` redirects to `/differentials`; `/?mode=dsm` redirects to `/dsm`; `/?mode=specifiers` redirects to `/specifiers`; `/?mode=formulation` redirects to `/formulation`. - **Intentionally composer-free routes**: `/differentials/presentations/*` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits. diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 261719ceb..e05b8924f 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -48,6 +48,7 @@ import { } from "@/lib/app-modes"; import { isLocalNoAuthMode, resolveClientDemoMode } from "@/lib/client-env"; import { documentsSearchHref } from "@/lib/document-flow-routes"; +import { isInformationPage } from "@/lib/information-pages"; import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; import { readSearchNavigationContext, type SearchNavigationOptions } from "@/lib/search-navigation-context"; import { shouldRenderClinicalDashboard, shouldRenderDashboardSearch } from "@/lib/search-route-ownership"; @@ -160,65 +161,6 @@ function GlobalSearchShellClient(props: GlobalSearchShellProps) { ); } -function isInformationPage(pathname: string): boolean { - // Services detail: /services/[slug] - if (pathname.startsWith("/services/") && pathname !== "/services") return true; - - // Forms detail: /forms/[slug] - if (pathname.startsWith("/forms/") && pathname !== "/forms") return true; - - // Medications detail: /medications/[slug] - if (pathname.startsWith("/medications/") && pathname !== "/medications") return true; - - // Psychiatric specifier detail: /specifiers/[slug] - if ( - pathname.startsWith("/specifiers/") && - pathname !== "/specifiers" && - pathname !== "/specifiers/builder" && - pathname !== "/specifiers/compare" && - pathname !== "/specifiers/map" - ) - return true; - - // Clinical formulation detail: /formulation/[slug] - if ( - pathname.startsWith("/formulation/") && - pathname !== "/formulation" && - pathname !== "/formulation/builder" && - pathname !== "/formulation/compare" && - pathname !== "/formulation/map" - ) - return true; - - // Factsheets detail: /factsheets/[slug] - if (pathname.startsWith("/factsheets/") && pathname !== "/factsheets" && pathname !== "/factsheets/search") - return true; - - // Therapy compass detail: /therapy-compass/[slug]/brief or /therapy-compass/[slug]/sheet - if ( - pathname.startsWith("/therapy-compass/") && - pathname !== "/therapy-compass" && - pathname !== "/therapy-compass/compare" && - pathname !== "/therapy-compass/pathways" && - pathname !== "/therapy-compass/recommend" && - pathname !== "/therapy-compass/review" && - pathname !== "/therapy-compass/search" - ) - return true; - - // Differential diagnosis detail: /differentials/diagnoses/[slug] or /differentials/presentations/[slug] - if (pathname.startsWith("/differentials/diagnoses/") || pathname.startsWith("/differentials/presentations/")) - return true; - - // DSM-5 Diagnosis detail: /dsm/diagnoses/[slug] or /dsm/diagnoses/[slug]/differentials or /dsm/compare - if (pathname.startsWith("/dsm/diagnoses/")) return true; - - // Document detail: /documents/[id] (excluding /documents/search) - if (pathname.startsWith("/documents/") && pathname !== "/documents/search") return true; - - return false; -} - function isToolDetailWithFooterSearch(pathname: string): boolean { return ( (pathname.startsWith("/services/") && pathname !== "/services") || diff --git a/src/components/clinical-dashboard/medication-record-page.tsx b/src/components/clinical-dashboard/medication-record-page.tsx index 42d973756..6c9ca862c 100644 --- a/src/components/clinical-dashboard/medication-record-page.tsx +++ b/src/components/clinical-dashboard/medication-record-page.tsx @@ -4,7 +4,6 @@ import { Activity, Ban, TriangleAlert, - ArrowLeft, BadgeCheck, BookOpen, CalendarDays, @@ -19,7 +18,6 @@ import { Timer, type LucideIcon, } from "lucide-react"; -import Link from "next/link"; import { useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { BadgeCluster } from "@/components/clinical-dashboard/clinical-badge"; @@ -46,12 +44,17 @@ import { cn, EmptyState, LoadingPanel, - pageContainer, toneDanger, toneInfo, toneSuccess, toneWarning, } from "@/components/ui-primitives"; +import { + InformationPageBreadcrumbs, + InformationPageFooter, + InformationPageShell, +} from "@/components/information-page-shell"; +import { appModeHomeHref } from "@/lib/app-modes"; const sectionIcons: Record = { dose: CalendarDays, @@ -387,7 +390,7 @@ function MedicationRecordDetail({ const activeSections = sectionsByTab[activeTab]; return ( -
+
@@ -496,27 +499,19 @@ export function MedicationRecordPage({ const governance = data?.governance ?? (error ? undefined : fallbackGovernance); return ( -
-
- -
-
+ + +
{record ? ( ) : loading ? ( -
- -
+ ) : (
@@ -526,9 +521,9 @@ export function MedicationRecordPage({
)}
-
+ Clinical KB provides evidence summaries, not medical advice. Verify clinical decisions. -
-
+ + ); } diff --git a/src/components/dsm/dsm-diagnosis-page.tsx b/src/components/dsm/dsm-diagnosis-page.tsx index f812f8219..b54c2c774 100644 --- a/src/components/dsm/dsm-diagnosis-page.tsx +++ b/src/components/dsm/dsm-diagnosis-page.tsx @@ -12,6 +12,7 @@ import { } from "lucide-react"; import { DsmHeaderActions, DsmPageHeader } from "@/components/dsm/dsm-page-header"; +import { InformationPageShell } from "@/components/information-page-shell"; import { cn, codeText, metadataPill, pageContainer } from "@/components/ui-primitives"; import { dsmCriteria, resolveDsmDifferential, type DsmDiagnosis, type DsmLabeledText } from "@/lib/dsm"; @@ -36,7 +37,7 @@ export function DsmDiagnosisPage({ diagnosis }: { diagnosis: DsmDiagnosis }) { const compareHref = `/dsm/compare?ids=${encodeURIComponent(diagnosis.slug)}`; return ( -
+ Supplied local catalogue
- + ); } diff --git a/src/components/dsm/dsm-page-header.tsx b/src/components/dsm/dsm-page-header.tsx index 384c01bac..983e54482 100644 --- a/src/components/dsm/dsm-page-header.tsx +++ b/src/components/dsm/dsm-page-header.tsx @@ -1,7 +1,8 @@ import Link from "next/link"; -import { ArrowLeft, BookOpenCheck, GitCompareArrows, Search } from "lucide-react"; +import { BookOpenCheck, GitCompareArrows, Search } from "lucide-react"; import type { ReactNode } from "react"; +import { InformationPageBreadcrumbs } from "@/components/information-page-shell"; import { cn, codeText, metadataPill, pageContainer } from "@/components/ui-primitives"; export function DsmPageHeader({ @@ -26,14 +27,8 @@ export function DsmPageHeader({
- - - DSM-5 Diagnosis home - -

+ +

{eyebrow}

@@ -41,11 +36,11 @@ export function DsmPageHeader({
-

+

{title}

{description ? ( -

+

{description}

) : null} diff --git a/src/components/factsheets/factsheet-detail-page.tsx b/src/components/factsheets/factsheet-detail-page.tsx index f688bb1c4..79db149d1 100644 --- a/src/components/factsheets/factsheet-detail-page.tsx +++ b/src/components/factsheets/factsheet-detail-page.tsx @@ -2,7 +2,6 @@ import Link from "next/link"; import { - ArrowLeft, ArrowUpRight, Bookmark, Check, @@ -28,6 +27,7 @@ import { type Factsheet, } from "@/components/factsheets/factsheets-data"; import { factsheetGlyph } from "@/components/factsheets/factsheets-icons"; +import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; import { cn, toneDanger, toneWarning } from "@/components/ui-primitives"; import { readSavedRegistrySlugs, @@ -105,17 +105,14 @@ export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) { return ( <> -
+ {/* action bar */}
- -
- +
); } diff --git a/src/components/specifiers/specifier-ui.tsx b/src/components/specifiers/specifier-ui.tsx index 101d842d8..d7b67c121 100644 --- a/src/components/specifiers/specifier-ui.tsx +++ b/src/components/specifiers/specifier-ui.tsx @@ -1,18 +1,9 @@ import Link from "next/link"; import type { ComponentType, CSSProperties, ReactNode } from "react"; -import { - ArrowLeft, - ArrowRight, - CheckCircle2, - ChevronRight, - ChevronsUpDown, - Info, - Minus, - ShieldAlert, - Tags, -} from "lucide-react"; +import { ArrowRight, CheckCircle2, ChevronsUpDown, Info, Minus, ShieldAlert, Tags } from "lucide-react"; -import { cn, eyebrowText, pageContainer } from "@/components/ui-primitives"; +import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; +import { cn, eyebrowText } from "@/components/ui-primitives"; import type { SpecifierFamily, SpecifierRecord } from "@/lib/specifiers"; import { specifierFamilies } from "@/lib/specifiers"; import type { SpecifierSourceStatus } from "@/lib/specifiers-search-index"; @@ -21,43 +12,11 @@ export const specifierCard = "rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-inset)]"; export function SpecifierPageShell({ children, className }: { children: ReactNode; className?: string }) { - return ( -
-
{children}
-
- ); + return {children}; } export function SpecifierBreadcrumbs({ current }: { current?: string }) { - return ( - - ); + return ; } export function SpecifierSubnav({ active }: { active: "search" | "builder" | "compare" | "map" }) { diff --git a/src/lib/information-pages.ts b/src/lib/information-pages.ts new file mode 100644 index 000000000..0f045d04e --- /dev/null +++ b/src/lib/information-pages.ts @@ -0,0 +1,78 @@ +/** + * Information pages are per-mode detail / record surfaces (not mode homes, builders, + * compare/map tools, or search results). The global search shell uses this to suppress + * the floating composer on read-focused routes. + * + * Basic chrome for these pages lives in `src/components/information-page-shell.tsx`. + * Intentional opt-outs from that shell (different product chrome): document viewer, + * therapy-compass CSS workspace, differentials presentation workflow. + */ + +export type InformationPageMode = + | "services" + | "forms" + | "prescribing" + | "specifiers" + | "formulation" + | "factsheets" + | "therapy-compass" + | "differentials" + | "dsm" + | "documents"; + +const TOOL_SUFFIXES = new Set(["builder", "compare", "map"]); + +function isSlugDetail(pathname: string, home: string, extraExcluded: string[] = []): boolean { + if (!pathname.startsWith(`${home}/`) || pathname === home) return false; + const rest = pathname.slice(home.length + 1); + if (!rest || rest.includes("/")) return false; + if (extraExcluded.includes(rest) || TOOL_SUFFIXES.has(rest)) return false; + return true; +} + +/** + * True when `pathname` is a mode information (detail/record) page. + * Keep in sync with adoption notes on `InformationPageShell`. + */ +export function isInformationPage(pathname: string): boolean { + if (isSlugDetail(pathname, "/services")) return true; + if (isSlugDetail(pathname, "/forms")) return true; + if (isSlugDetail(pathname, "/medications")) return true; + if (isSlugDetail(pathname, "/specifiers")) return true; + if (isSlugDetail(pathname, "/formulation")) return true; + if (isSlugDetail(pathname, "/factsheets", ["search"])) return true; + + // Therapy compass detail: /therapy-compass/[slug]/brief or /sheet (and bare slug if present) + if ( + pathname.startsWith("/therapy-compass/") && + pathname !== "/therapy-compass" && + pathname !== "/therapy-compass/compare" && + pathname !== "/therapy-compass/pathways" && + pathname !== "/therapy-compass/recommend" && + pathname !== "/therapy-compass/review" && + pathname !== "/therapy-compass/search" + ) { + return true; + } + + if (pathname.startsWith("/differentials/diagnoses/") || pathname.startsWith("/differentials/presentations/")) { + return true; + } + + if (pathname.startsWith("/dsm/diagnoses/")) return true; + + if (pathname.startsWith("/documents/") && pathname !== "/documents/search") return true; + + return false; +} + +/** Modes that use the shared `InformationPageShell` for outer chrome. */ +export const informationPageShellModes = [ + "services", + "forms", + "prescribing", + "specifiers", + "formulation", + "factsheets", + "dsm", +] as const satisfies readonly InformationPageMode[]; diff --git a/tests/forms-back-navigation.dom.test.tsx b/tests/forms-back-navigation.dom.test.tsx index a668315c4..136cfbc64 100644 --- a/tests/forms-back-navigation.dom.test.tsx +++ b/tests/forms-back-navigation.dom.test.tsx @@ -1,18 +1,9 @@ -import { fireEvent, render, screen } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; import { FormDetailPage } from "@/components/forms/form-detail-page"; import { formRecords } from "@/lib/forms"; -const router = vi.hoisted(() => ({ - back: vi.fn(), - push: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - useRouter: () => router, -})); - vi.mock("@/components/account-data-provider", () => ({ useAccountData: () => ({ isSaved: () => false, @@ -20,22 +11,10 @@ vi.mock("@/components/account-data-provider", () => ({ }), })); -beforeEach(() => { - router.back.mockReset(); - router.push.mockReset(); -}); - describe("Form detail back navigation", () => { - it("uses the canonical Forms route even when browser history has prior entries", () => { - window.history.pushState({}, "", "/unrelated-route"); - window.history.pushState({}, "", "/forms/transport-crisis-form"); - expect(window.history.length).toBeGreaterThan(1); - + it("links to the canonical Forms home with focus", () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Back to forms" })); - - expect(router.push).toHaveBeenCalledOnce(); - expect(router.push).toHaveBeenCalledWith("/forms?focus=1"); - expect(router.back).not.toHaveBeenCalled(); + const back = screen.getByRole("link", { name: /Forms/i }); + expect(back).toHaveAttribute("href", "/forms?focus=1"); }); }); diff --git a/tests/information-page-shell.test.tsx b/tests/information-page-shell.test.tsx new file mode 100644 index 000000000..701e612fb --- /dev/null +++ b/tests/information-page-shell.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; +import { isInformationPage } from "@/lib/information-pages"; + +describe("isInformationPage", () => { + it("recognises catalogue detail routes per mode", () => { + expect(isInformationPage("/services/acuity")).toBe(true); + expect(isInformationPage("/forms/transport-crisis-form")).toBe(true); + expect(isInformationPage("/medications/lithium")).toBe(true); + expect(isInformationPage("/specifiers/with-anxious-distress")).toBe(true); + expect(isInformationPage("/formulation/avoidance")).toBe(true); + expect(isInformationPage("/factsheets/ssri-start")).toBe(true); + expect(isInformationPage("/dsm/diagnoses/mdd")).toBe(true); + expect(isInformationPage("/differentials/diagnoses/delirium")).toBe(true); + expect(isInformationPage("/documents/abc")).toBe(true); + }); + + it("excludes mode homes, builders, and search surfaces", () => { + expect(isInformationPage("/services")).toBe(false); + expect(isInformationPage("/forms")).toBe(false); + expect(isInformationPage("/specifiers/builder")).toBe(false); + expect(isInformationPage("/formulation/compare")).toBe(false); + expect(isInformationPage("/factsheets/search")).toBe(false); + expect(isInformationPage("/documents/search")).toBe(false); + expect(isInformationPage("/therapy-compass/search")).toBe(false); + }); +}); + +describe("InformationPageShell", () => { + it("renders a main landmark with the default container", () => { + render( + +

Body

+
, + ); + const main = screen.getByTestId("info-shell"); + expect(main.tagName).toBe("MAIN"); + expect(main.querySelector(".max-w-7xl")).not.toBeNull(); + expect(screen.getByText("Body")).toBeInTheDocument(); + }); + + it("exposes shared breadcrumbs as a link back to the mode home", () => { + render( + , + ); + const home = screen.getByRole("link", { name: /Forms/i }); + expect(home).toHaveAttribute("href", "/forms?focus=1"); + expect(screen.getByText("Transport")).toHaveAttribute("aria-current", "page"); + }); +}); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 20fdd89d6..49c36ce06 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2905,7 +2905,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await acamprosateResult.click(); await expect(page).toHaveURL(/\/medications\/acamprosate$/, { timeout: 30_000 }); await expectSingleMedicationPage(page); - await expect(page.getByRole("link", { name: "Back to medication search" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Medications" })).toBeVisible(); expect(parentNodeErrors).toEqual([]); }); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index b80984204..5592814fe 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1939,7 +1939,7 @@ test.describe("Clinical KB service detail page", () => { const copyContactButton = servicePage.getByRole("button", { name: "Copy contact" }).last(); await expect(servicePage).toBeVisible(); await expect(servicePage.getByRole("heading", { level: 1, name: "13YARN" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Back to services" })).toBeVisible(); + await expect(page.getByRole("link", { name: "Services" })).toBeVisible(); await expect(servicePage.getByRole("button", { name: "Save service" })).toBeVisible(); await expect(copyContactButton).toBeVisible(); await expect(servicePage.getByRole("link", { name: "Call" })).toHaveAttribute("href", "tel:139276"); @@ -2033,7 +2033,7 @@ test.describe("Clinical KB service detail page", () => { await servicePage.getByRole("button", { name: "Copy contact" }).last().click(); await expect(page.getByRole("status")).toContainText("Contact copied"); - await page.getByRole("button", { name: "Back to services" }).click(); + await page.getByRole("link", { name: "Services" }).click(); await expect(page).toHaveURL(/\/services(?:\?|$)/); }); }); From f0071e25f467986ee2695393774fedf13d05c249 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 07:02:17 +0000 Subject: [PATCH 2/6] Update dock min-height guard for shared information-page shell. Point the merge-artifacts phone padding assertions at InformationPageShell now that catalogue detail pages consume that shared chrome. Co-authored-by: BigSimmo --- tests/clinical-dashboard-merge-artifacts.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/clinical-dashboard-merge-artifacts.test.ts b/tests/clinical-dashboard-merge-artifacts.test.ts index ec7ec4bdb..06da87b94 100644 --- a/tests/clinical-dashboard-merge-artifacts.test.ts +++ b/tests/clinical-dashboard-merge-artifacts.test.ts @@ -50,6 +50,10 @@ const formulationUiSource = readFileSync( resolve(process.cwd(), "src/components/formulation/formulation-ui.tsx"), "utf8", ); +const informationPageShellSource = readFileSync( + resolve(process.cwd(), "src/components/information-page-shell.tsx"), + "utf8", +); const favouritesLibrarySource = readFileSync( resolve(process.cwd(), "src/components/clinical-dashboard/favourites-command-library-page.tsx"), "utf8", @@ -167,13 +171,16 @@ describe("ClinicalDashboard merge-artifact guards", () => { expect(differentialsHomeSource).not.toContain("pb-[calc(12.5rem+env(safe-area-inset-bottom))]"); expect(applicationsLauncherSource).not.toContain("pb-[calc(12rem+env(safe-area-inset-bottom))]"); expect(serviceDetailSource).not.toContain("pb-[calc(5.5rem+env(safe-area-inset-bottom))]"); - expect(serviceDetailSource).toContain("max-sm:min-h-0"); + expect(serviceDetailSource).toContain("InformationPageShell"); expect(formDetailSource).not.toContain("pb-[calc(2rem+env(safe-area-inset-bottom))]"); - expect(formDetailSource).toContain("max-sm:min-h-0"); + expect(formDetailSource).toContain("InformationPageShell"); expect(specifierUiSource).not.toContain("pb-[calc(7rem+env(safe-area-inset-bottom))]"); - expect(specifierUiSource).toContain("max-sm:min-h-0"); + expect(specifierUiSource).toContain("InformationPageShell"); expect(formulationUiSource).not.toContain("pb-[calc(7rem+env(safe-area-inset-bottom))]"); - expect(formulationUiSource).toContain("max-sm:min-h-0"); + expect(formulationUiSource).toContain("InformationPageShell"); + // Phone min-height contract lives on the shared information-page shell so + // catalogue detail pages do not each reintroduce a 100dvh pad under the dock. + expect(informationPageShellSource).toContain("max-sm:min-h-0"); expect(favouritesLibrarySource).not.toContain("pb-[calc(6rem+env(safe-area-inset-bottom))]"); expect(globalStylesSource).toContain("--phone-dock-hidden-pad: 0rem"); }); From a7a3b6b134fa989c7243f5aac569c33d6ccb51f5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 07:40:39 +0000 Subject: [PATCH 3/6] test: align information page navigation assertions Co-authored-by: BigSimmo --- tests/ui-smoke.spec.ts | 2 +- tests/ui-tools.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 49c36ce06..903d57247 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2937,7 +2937,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await acamprosateCard.click(); await expect(page).toHaveURL(/\/medications\/acamprosate$/, { timeout: 30_000 }); - const backLink = page.getByRole("link", { name: "Back", exact: true }); + const backLink = page.getByRole("link", { name: "Medications", exact: true }); await expect(backLink).toBeVisible(); await expectMinTouchTarget(backLink); await backLink.click(); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 5592814fe..26f1fb376 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1939,7 +1939,7 @@ test.describe("Clinical KB service detail page", () => { const copyContactButton = servicePage.getByRole("button", { name: "Copy contact" }).last(); await expect(servicePage).toBeVisible(); await expect(servicePage.getByRole("heading", { level: 1, name: "13YARN" })).toBeVisible(); - await expect(page.getByRole("link", { name: "Services" })).toBeVisible(); + await expect(servicePage.getByRole("link", { name: "Services" })).toBeVisible(); await expect(servicePage.getByRole("button", { name: "Save service" })).toBeVisible(); await expect(copyContactButton).toBeVisible(); await expect(servicePage.getByRole("link", { name: "Call" })).toHaveAttribute("href", "tel:139276"); @@ -2033,7 +2033,7 @@ test.describe("Clinical KB service detail page", () => { await servicePage.getByRole("button", { name: "Copy contact" }).last().click(); await expect(page.getByRole("status")).toContainText("Contact copied"); - await page.getByRole("link", { name: "Services" }).click(); + await servicePage.getByRole("link", { name: "Services" }).click(); await expect(page).toHaveURL(/\/services(?:\?|$)/); }); }); From 5666be010d6bad76907dddf0f54eb2d6b529df3f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:26:11 +0800 Subject: [PATCH 4/6] docs: record Run PR behind-main sweep for #1148 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 8717bb536..850292dc8 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -731,3 +731,4 @@ This file is append-only. Never rewrite or delete an existing review record; app - Outcome: No new P0/P1 search chrome defect found in the static review. Fixed one regression hazard: a stale ClinicalDashboard comment still instructed a 0.75rem hidden dock pad despite the implementation/tests requiring 0rem. Added durable search chrome behaviour rules in AGENTS.md and docs/search-chrome-behaviour.md, with a static guard tying the remembered rules to the hidden-reserve contract. - Checks: dependency shortcut section count; git diff --check; targeted rg for stale 0.75rem hidden-pad source wording (only negative test assertions remain); targeted Vitest command attempted but blocked by missing node_modules/vitest under Node 20.20.2 in this container. No provider-backed checks run. | 2026-07-24 | cursor/search-interactive-perf-af54 (PR #1138) | ff4b293d95f922e70ebf5ee9b0c156c41a8bff3b | Run PR sweep: CI fix + threads + drift | Before: CONFLICTING, CI green, 0 threads. RAG impact: no retrieval behaviour change — PR is client deferred-search/UI only (no src/lib/rag/**). After: merged origin/main; conflict resolved in src/components/ui/sheet.tsx by keeping main restoreTimersRef/unmountingRef focus-restore fix; pushed ff4b293d9. Threads: none. | merge origin/main only; no provider-backed checks run | +| 2026-07-24 | cursor/information-page-structure-2a5d (PR #1148) | 97511d69256b97de4f4e654ff6c12f3742f795f4 | Run PR sweep: CI fix + threads + drift | Before: behind main. After: merged origin/main cleanly (no conflicts). Unresolved review threads left as non-P0/P1. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | From c93826648d5224fb23b8c1df48d95c00b80d7b6d Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:57:50 +0800 Subject: [PATCH 5/6] docs: ledger re-sync sweep for PR #1148 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 58fa24618..68cf7db96 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -738,3 +738,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | remediate-audit-system-issues (PR #1160) | 04201a87cc7ad7dd1477d17cd2b96544b7379789 | Run PR sweep: CI fix + threads + drift | re-merge origin/main after CONFLICTING relapse; resolved scripts/run-eval-safe.mjs (kept main taskkill /T /F + validPids); sitemap Prettier format() retained from fe0588b86; no unresolved threads; no conflict markers | merge + conflict resolve only; no provider-backed checks run | | 2026-07-24 | codex/fix-next.js-startup-failure-and-verify-pages (PR #1149) | 8ddddbab2a29a94b3f993cbd114889f72c95f4f1 | Run PR sweep: CI fix + threads + drift | Before: behind main. After: merged origin/main cleanly (no conflicts). Unresolved review threads left as non-P0/P1. CI not waited. | merge origin/main only; thread scan read-only; no provider-backed checks run | | 2026-07-24 | remediate-audit-system-issues (PR #1160) | bdf530fc8c6faaa4491c510396b47872fc39bf25 | Run PR sweep: CI fix + threads + drift | second re-merge after main moved to 2e68888f3 during first push; clean ort merge (ledger + layout.tsx); taskkill /T retained; sitemap prettier retained | merge only; no provider-backed checks run | +| 2026-07-24 | cursor/information-page-structure-2a5d (PR #1148) | 230770a53f9bfb53ba69abc81d6e8491e021f8a9 | Run PR re-sync sweep | Before: CONFLICTING. After: merged origin/main clean. | merge origin/main and/or conflict re-check only; no provider-backed checks run | From cbff6850edf05407f9b586a1d835a510df646f54 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 19:08:57 +0000 Subject: [PATCH 6/6] style(docs): format codebase-index.md for Prettier CI Co-authored-by: BigSimmo --- docs/codebase-index.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 5da29a82f..a66138c60 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -304,11 +304,14 @@ sequenceDiagram One shared composer (`master-search-header.tsx`) serves every mode. Placement: <<<<<<< HEAD + - **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/formulation`, `/tools`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike. (`/applications` is a redirect to `/tools`, not a composer surface.) - **Information (detail) pages**: catalogue/record routes under each mode (`/services/[slug]`, `/forms/[slug]`, `/medications/[slug]`, `/specifiers/[slug]`, `/formulation/[slug]`, `/factsheets/[slug]`, `/dsm/diagnoses/[slug]`, …). Route detection: `src/lib/information-pages.ts` (`isInformationPage`). Shared outer chrome: `src/components/information-page-shell.tsx` (`InformationPageShell`, breadcrumbs, optional footer). Specifier/formulation mode shells re-export that primitive. Intentional opt-outs: document viewer, therapy-compass CSS workspace, differentials presentation workflow. -======= + \======= - **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/dsm`, `/specifiers`, `/formulation`, `/factsheets`, `/therapy-compass`, `/tools`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike. (`/applications` is a redirect to `/tools`, not a composer surface.) ->>>>>>> origin/main + +> > > > > > > origin/main + - **Result and detail views**: fixed bottom dock on phone (compact variant on submitted searches), sticky top from `sm` up. - **Results routing**: standalone routes own their submitted searches via `?q=…&run=1` (`/services` → `ServicesNavigatorPage`, `/forms` → `FormsSearchResultsPage`, `/differentials` → `DifferentialsHome` results view, `/formulation` → local mechanism results, `/favourites` filters the command library in place). Answer, Documents, and Prescribing submitted searches render inside `ClinicalDashboard` — intentional, since they need retrieval/answer state. `/?mode=favourites` redirects to `/favourites`; `/?mode=differentials` redirects to `/differentials`; `/?mode=dsm` redirects to `/dsm`; `/?mode=specifiers` redirects to `/specifiers`; `/?mode=formulation` redirects to `/formulation`. - **Intentionally composer-free routes**: `/differentials/presentations/*` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits.