From 249526988ea3d65c54e69ee7ca05e514bff50ed8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:52:19 +0800 Subject: [PATCH 1/3] feat(in-page-nav): convert the six information routes onto the shared header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 2 of the in-page navigation series. PR 1 (#1740) extracted the DocumentViewer in-page navigation template into src/components/in-page-nav/ and converted the differentials detail page as proof. This converts the six routes still drawing the shell-owned "On this page" pill rail — services, forms, specifiers (record and catalogue reference), formulation, and the two DSM diagnosis routes — then deletes the rail behind them. Three things the conversion needed first: - InPageNavHeader's `actions` was a render prop. Four of the seven components in scope are Server Components, and React cannot pass a function across that boundary, so `actions` is widened to `ReactNode | ((close) => ReactNode)`. `onSelectSection` and `PageSection.icon` have the same problem, so those four pages mount the header through a small "use client" sibling module that owns the section table and the hooks and takes `actions` as a slot. - Both sheets now derive their open state from the current pathname, so navigating closes them. Server-passed action JSX is mostly s and has no way to call close(). - Information-page sections carried no scroll-mt at all. A shared `inPageAnchor` token consumes `--inpage-anchor-offset`, published from the live chrome height by useInPageChromeMetrics, which InPageNavHeader calls itself. The measurement behind both anchor offsets is now useStickyChromeMetrics in src/components/sticky-chrome-metrics.ts, shared with the document viewer. use-document-chrome-metrics.ts keeps its selector and custom-property literals verbatim, because header-scroll-hide-contract.test.ts pins that file's source. Two DSM routes declared section anchors that nothing rendered, so /dsm/diagnoses//differentials drew one nav entry out of four declared and /dsm/diagnoses/ drew three out of five. Both are wired in, asserted against rendered DOM rather than a source grep (/issues #256's stop rule) by the new tests/in-page-nav-route-sections.dom.test.tsx, which covers all seven components including present-and-absent cases for the conditional anchors. Rail removal: hasLocalInformationPageNavigation collapses to isInformationPage, because every information page now owns its navigation; the `section` kind and its scroll spy leave secondary-navigation.tsx. That leaves the component with no production constructor at all — RegistryModeNav renders ModeNav, not SecondaryNavigation — which is folded into /issues #271 rather than resolved here. Also corrects /issues #256: its Forms half was stale, all six form anchors render. Records /issues #285 for a pre-existing ui-smoke phone scroll-hide flake at base commit 9ab3b73a, proven on pristine source in the same worktree. Co-Authored-By: Claude Opus 5 --- docs/codebase-index.md | 3 +- docs/outstanding-issues.md | 7 +- docs/search-chrome-behaviour.md | 29 + .../global-search-shell.tsx | 20 +- .../differential-detail-page.tsx | 4 +- .../use-document-chrome-metrics.ts | 90 +-- .../dsm/dsm-diagnosis-nav-header.tsx | 47 ++ src/components/dsm/dsm-diagnosis-page.tsx | 475 ++++++------ .../dsm-differential-considerations-page.tsx | 456 +++++++----- src/components/dsm/dsm-page-header.tsx | 36 +- src/components/forms/form-detail-page.tsx | 699 +++++++++--------- .../formulation-mechanism-page.tsx | 450 +++++------ .../formulation/formulation-nav-header.tsx | 38 + src/components/formulation/formulation-ui.tsx | 11 +- .../in-page-nav/in-page-nav-classes.ts | 32 + .../in-page-nav/in-page-nav-header.tsx | 40 +- .../in-page-nav/use-in-page-chrome-metrics.ts | 30 + .../in-page-nav/use-in-page-section-nav.ts | 77 ++ .../in-page-nav/use-resolved-page-sections.ts | 86 +++ src/components/mode-nav/header-addon-slot.ts | 20 + src/components/page-secondary-navigation.tsx | 224 +----- src/components/secondary-navigation.tsx | 152 +--- .../services/service-detail-page.tsx | 540 +++++++------- .../specifiers/specifier-nav-header.tsx | 49 ++ .../specifiers/specifier-record-page.tsx | 272 +++---- .../specifiers/specifier-reference-page.tsx | 316 ++++---- src/components/specifiers/specifier-ui.tsx | 11 +- src/components/sticky-chrome-metrics.ts | 136 ++++ ...audit-content-services-regressions.test.ts | 8 +- tests/in-page-nav-header.dom.test.tsx | 48 ++ tests/in-page-nav-route-sections.dom.test.tsx | 200 +++++ tests/mode-nav-addon-slot.dom.test.tsx | 37 +- tests/page-secondary-navigation.dom.test.tsx | 197 +---- tests/secondary-navigation.dom.test.tsx | 110 +-- tests/ui-forms-section-nav.spec.ts | 57 +- tests/ui-formulation.spec.ts | 21 +- tests/ui-smoke.spec.ts | 4 +- tests/ui-specifiers.spec.ts | 13 +- tests/ui-tools.spec.ts | 38 +- 39 files changed, 2777 insertions(+), 2306 deletions(-) create mode 100644 src/components/dsm/dsm-diagnosis-nav-header.tsx create mode 100644 src/components/formulation/formulation-nav-header.tsx create mode 100644 src/components/in-page-nav/in-page-nav-classes.ts create mode 100644 src/components/in-page-nav/use-in-page-chrome-metrics.ts create mode 100644 src/components/in-page-nav/use-in-page-section-nav.ts create mode 100644 src/components/in-page-nav/use-resolved-page-sections.ts create mode 100644 src/components/specifiers/specifier-nav-header.tsx create mode 100644 src/components/sticky-chrome-metrics.ts create mode 100644 tests/in-page-nav-route-sections.dom.test.tsx diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 48d865a4f..01d648bbb 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -329,7 +329,8 @@ One shared composer (`master-search-header.tsx`) serves every mode. Placement: - **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. Bare `/?mode=` always renders the shared home with that mode preselected; only a submitted deep link (`q` plus `run=1`) resolves to the mode's own search surface (proxy early-redirect still covers favourites/differentials/specifiers for those submitted aliases). - **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. -- **Shared secondary navigation**: `src/components/secondary-navigation.tsx` (`SecondaryNavigation`, route/section/action items, roving tablist, fragment section tracking) and `src/components/page-secondary-navigation.tsx` (`PageSecondaryNavigation`, per-route selection of mode destinations vs. "On this page" section anchors). Mode destinations come from `src/lib/mode-secondary-navigation.ts` (`modeSecondaryNavigationRegistry`, no "Home" item). `GlobalSearchShell` renders it in normal flow at the top of `#main-content` for its owned namespaced modes; it self-suppresses on clean mode homes, locally-owned detail routes (medications, factsheets, differentials diagnoses) and Therapy Compass, and Specifiers/Formulation keep their existing local `Subnav` (so the shared mode bar is skipped for those two modes to avoid a duplicate row). +- **Shared in-page navigation**: `src/components/in-page-nav/` is the default template for section navigation on any mode page (`docs/search-chrome-behaviour.md`). `in-page-nav-header.tsx` (`InPageNavHeader`) owns the header row, both sheets and the `PhoneHeaderCollapsePortal` wrapper; `page-section-index.ts` (`PageSection`, `toDocumentSections`, `sectionTargetIds`) is the declaration shape; `use-resolved-page-sections.ts` narrows a declaration to the anchors actually rendered at this breakpoint; `use-in-page-section-nav.ts` composes that with `useDocumentSectionSpy` and `jumpToDocumentSection`; `use-page-section-weights.ts` measures segment weights; `use-in-page-chrome-metrics.ts` publishes `--inpage-anchor-offset`; `in-page-nav-classes.ts` holds the shared anchor (`inPageAnchor`) and actions-sheet row classes. Anchor measurement itself is `src/components/sticky-chrome-metrics.ts` (`useStickyChromeMetrics`), shared with the document viewer's `use-document-chrome-metrics.ts`. Mounted by `differentials/differential-detail-page.tsx`, `services/service-detail-page.tsx`, `forms/form-detail-page.tsx`, `dsm/dsm-differential-considerations-page.tsx`, and — through a `"use client"` sibling module, because those pages are Server Components — `specifiers/specifier-nav-header.tsx`, `formulation/formulation-nav-header.tsx` and `dsm/dsm-diagnosis-nav-header.tsx`. Every declared section is pinned against rendered DOM by `tests/in-page-nav-route-sections.dom.test.tsx`. +- **Shared secondary navigation**: `src/components/secondary-navigation.tsx` (`SecondaryNavigation`, route/action items, roving tablist) and `src/components/page-secondary-navigation.tsx` (`PageSecondaryNavigation`, mode destinations only). Mode destinations come from `src/lib/mode-secondary-navigation.ts` (`modeSecondaryNavigationRegistry`, no "Home" item). `GlobalSearchShell` renders it in normal flow at the top of `#main-content` for its owned namespaced modes; it self-suppresses on clean mode homes, on Therapy Compass, and on every information page — `hasLocalInformationPageNavigation` is now just `isInformationPage`, because each of those routes owns its own in-page navigation. The `section` kind and its "On this page" pill rail were removed once the last six routes moved onto `InPageNavHeader`; `route`/`action` survive as component API with tests but have no production constructor (`/issues #271`). - **Local filter fields** (sidebar "Search chats", document drawer "Find a document"/"Find a source PDF") are scoped filters, not global search; they share the `fieldControlWithIcon`/`fieldIcon` primitives. - **Wiring conventions** for buttons and route navigation (and the gates that enforce them — the dead-button ESLint rule and the orphan-route reachability test) live in `docs/wiring-conventions.md`. diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 4b8f7dab3..3a82ca853 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -165,7 +165,7 @@ removed after current-main verification; it is not missing recommended work. | 112 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | - + ## Open items > **Merged-main canary update (2026-07-23, run `30018289898`):** the new structured report correctly recorded evaluated tree `c24f2e8f2d30d0c59fc1eba025d3dcd63478137e`, run/attempt identity and `cross-region-runner` latency context. Golden retrieval remained 36/36 with document/content recall 1.0 and no failed cases. The 44-case answer gate had grounded-supported and unsupported-correct rates of 1.0, but failed because `neuroleptic-side-effect-escalation` again returned one citation where two are required (citation-failure rate 0.0227). `admission-discharge-comparison` again omitted the specific AKG admission document after `comparison_source_extractive_fallback`; `admission-discharge-coverage-paraphrase` was advisory-only at 24,870 ms. Answer cost was reported as `$0.234736`. Do not retry immediately: retain this as the first structured datapoint, compare it with the scheduled 2026-07-26 report, and keep retrieval/ranking unchanged. @@ -295,7 +295,7 @@ removed after current-main verification; it is not missing recommended work. | #253 | P3 | task | #1606 needs a hand-merge against merged PR #1615, not a rebase | SUPERSEDED IN PART 2026-08-07: the component both PRs rewrite no longer exists. `MobileResultFilterControl` — the native `