feat(navigation): roll the shared mode nav out to DSM, Specifiers, Formulation and Differentials - #1647
Conversation
…t ownership Starts the site-wide rollout of the header-integrated `ModeNav` that #1642 shipped for Therapy. DSM first: two destinations, so no More slot and no overflow band to calibrate. `RegistryModeNav` adapts the canonical `modeSecondaryNavigationRegistry` to `ModeNavItem` rather than each mode hand-writing a list, so the shell and the mode cannot drift onto different URLs for the same tab. Its icon map is exhaustive by a type derived from the registry literal: adding a routed entry without choosing an icon fails the typecheck instead of shipping a document icon nobody picked. Also closes an invariant that until now held by accident. The header addon slot takes ONE page-owned header, and what kept `documents` and `differentials` clear was that both had fewer than two destinations, so `ModeNav` rendered nothing. That protection expires the moment a claimant's mode gains one. `/differentials/diagnoses/<slug>` is the live case: it is locally-owned information navigation, so it skips the sections branch, and a submitted search makes `isModeSecondaryNavigationRoute` true ahead of every per-mode check — mounting a second header into a slot `DifferentialDetailPage` owns. `isHeaderAddonSlotOwnedRoute` now names the claimants, and the render-level test asserts one occupant rather than inferring it from the item count. Adoption is an explicit per-mode list, not a "has two routed entries" derivation, so a registry edit cannot silently move a mode onto a different navigation surface. The eight single-destination modes keep their existing `SecondaryNavigation` strip untouched — dropping it is a deletion decision about their lone entry, not part of this port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S9mA7QhMw1hpZCVMzH7pZL
…e bar Replaces the two page-owned strips (`SpecifierSubnav`, `FormulationSubnav`) with the header-integrated bar, and drops the shell exclusion that skipped `PageSecondaryNavigation` for both modes — that exclusion existed only to stop a duplicate row next to those strips, and with the strips gone it would have suppressed the modes' only navigation. Desktop labels shorten: "Build wording" / "Build formulation" / "Mechanism map" become the registry's "Build" / "Map". `ModeNavItem` has no `shortLabel` by design (a slot shows its real word or folds into More), and the registry is now the single source of the wording. Lengthening it instead was the alternative, but "Build formulation" plus "Mechanism map" do not fit the calibrated 42rem band, and ledger #113 is explicit that the thresholds are measured, not padding to be widened. Both mode homes go bar-free, as every mode home is: `/specifiers` and `/formulation` already surface builder, compare and map as tiles, so nothing is stranded by the removal. Their `[slug]` detail routes keep the information-page sections nav they share with every other record route, rather than the mode strip they used to carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S9mA7QhMw1hpZCVMzH7pZL
Completes the four multi-destination modes. Differentials is last because it is the only one with a route that portals its own header into the universal header's single addon slot, and that slot takes one occupant. The protection there was never stated. It used to be that `documents` and `differentials` — the only modes with a claimant — had fewer than MODE_NAV_MIN_ITEMS destinations, so `ModeNav` rendered nothing; Differentials now has three. What holds it today is a second coincidence: every claimant route is also `hasLocalInformationPageNavigation`, which `PageSecondaryNavigation` returns null on well before the mode branch. Two separately maintained lists that happen to agree. Rather than add a guard that cannot execute, `isHeaderAddonSlotOwnedRoute` names the claimants and the test asserts the agreement route for route, plus the render-level outcome. Breaking the cover fails three assertions; the earlier explicit guard failed none, which is what showed it was unreachable. When a future claimant falls outside the cover, that is the signal the mode branch needs a guard of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S9mA7QhMw1hpZCVMzH7pZL
… mode The spec's header promises that a mode whose labels are longer than the thresholds allow fails there rather than shipping clipped words. Every route it drove was `/therapy-compass/*`, so that promise covered exactly one mode. Four more have now adopted the bar; their labels are short enough to pass either way, which is why the gap survives until a mode with long ones arrives. Each mode is loaded at every band boundary and one pixel either side, with the expected slot count derived as `min(items, capacity)` rather than tabulated, so a mode with a different destination count needs no second table. Two cases are new behaviour, not just new coverage: Specifiers and Formulation carry four destinations, so at the 22rem band their last item folds and the More slot has to carry the current page's rule. That is invariant I5's container-query branch, and no route outside Therapy exercised it before. `tests/mode-nav-contract.test.ts` pins the spec's mode table against MODE_NAV_ADOPTED_MODES and the registry, so adopting a mode or adding a destination without extending this spec fails offline instead of leaving a mode unmeasured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S9mA7QhMw1hpZCVMzH7pZL
This pull request has been ignored for the connected project Preview Branches by Supabase. |
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughSpecifier and Formulation pages now use shared registry-driven navigation. Local subnavigation was removed, section anchors were added, and route ownership plus active-state handling were expanded. Tests cover navigation ownership, registry coverage, page targets, and responsive density. ChangesSecondary navigation migration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Page
participant PageSecondaryNavigation
participant RegistryModeNav
participant ModeNav
Page->>PageSecondaryNavigation: provide mode and pathname
PageSecondaryNavigation->>RegistryModeNav: select adopted-mode navigation
RegistryModeNav->>ModeNav: pass registry items and activeId
ModeNav-->>PageSecondaryNavigation: render shared navigation
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
CI triageCI failed on this PR. Automated classification of the 4 failed job(s):
Compared with main CI run #8427 (cancelled). Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
BigSimmo
commented
Aug 6, 2026
@copilot Fix the code for all comments in this review thread. When a review comment includes a suggested change, apply the suggestion exactly. Do not make changes beyond what is described in the linked review thread. |
…ption (#1647) Wire the declared specifier/formulation information-section IDs into the record pages so "On this page" actually renders after Subnav removal. Also pin MODE_NAV_ADOPTED_MODES to the ≥2 routed-entry criterion, and stop marking Find/Search current on unmatched in-mode routes. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
Implemented the requested review-thread fix in |
BigSimmo
commented
Aug 6, 2026
Review disposition (follow-up on Devin + pasted findings)Addressed in
Not changing: putting header |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
The adoption test asserted that every listed mode has two or more routed entries, and that modes with fewer stay off the list. Neither half notices a mode being removed: drop `formulation` and the remaining three still satisfy the criterion, while the negative check only inspects modes with fewer than two entries. A mode silently losing its only navigation is precisely the regression the explicit list exists to prevent, so pin the membership as well. Raised in review. Confirmed by mutation: removing `formulation` passed before this, and fails after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S9mA7QhMw1hpZCVMzH7pZL
`Production UI` caught this; my local run of the spec predated the commit that
caused it. Wiring up the declared section anchors made `/formulation/<slug>`
render an "On this page" nav, and its link carries the same words as the
record's own section label — so `getByText("What matters now", { exact: true })`
now resolves to two elements and fails Playwright strict mode.
The assertion wants the record's label, so address it by
`#formulation-what-matters-now-label` rather than by text that two elements
legitimately share. `ui-specifiers.spec.ts:93` uses the same text but is not
affected: "What matters now" is not one of the specifier section labels, so no
nav link carries it there. Confirmed by running both specs, 12 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S9mA7QhMw1hpZCVMzH7pZL…tion flake sighting) and widen #210
…claude/new-session-6fz57i
Empty commit to re-fire pull_request synchronize for the current tip after stale/queued runs were cancelled during the Actions major outage. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Empty commit to fire pull_request synchronize after head had no Actions runs. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/branch-review-ledger.md`:
- Around line 675-684: The branch-review ledger must remain append-only: restore
the existing temp-rebase, PR `#1649`, and PR `#1641` rows to their prior order, then
add only the new PR `#1647` entry using the repository ledger command. Do not
hand-edit, delete, or reorder existing rows, and run post-main deduplication
only when the documented workflow requires it.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 684db5e7-c781-4e88-a642-dafecfbff7e7
📒 Files selected for processing (7)
docs/branch-review-ledger.mddocs/outstanding-issues.mdsrc/components/clinical-dashboard/global-search-shell.tsxsrc/lib/mode-secondary-navigation.tstests/mode-secondary-navigation.test.tstests/ui-formulation.spec.tstests/ui-specifiers.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/components/clinical-dashboard/global-search-shell.tsx
- tests/mode-secondary-navigation.test.ts
- src/lib/mode-secondary-navigation.ts
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Summary
Completes the rollout #1642 started for Therapy. The four multi-destination modes now use the shared header-integrated
ModeNavinstead of an in-flow strip or a page-owned subnav.RegistryModeNavadapter — mapsmodeSecondaryNavigationRegistryontoModeNavItemso the shell and the mode cannot drift onto different URLs for the same tab. Its icon map is keyed by a type derived from the registry literal, so adding a routed entry without choosing an icon fails the typecheck rather than shipping aFileTextfallback nobody picked. Action-only entries are dropped, not adapted —ModeNavItemtakes anhrefby design.SpecifierSubnavandFormulationSubnavare removed, along with theGlobalSearchShellexclusion that skippedPageSecondaryNavigationfor both modes (it existed only to prevent a duplicate row next to those strips; with them gone it would have suppressed the modes' only navigation).7891fa7) — the declared specifier/formulation section target ids are now rendered on the record, reference and mechanism pages. See "A regression this PR introduced and fixed" below.tests/ui-mode-nav-density.spec.tspreviously drove only/therapy-compass/*.Adoption is an explicit list (
MODE_NAV_ADOPTED_MODES), not a "has two or more routed entries" derivation, so a registry edit cannot silently move a mode onto a different navigation surface. Its membership is pinned by test, not just the criterion — the criterion alone is satisfied by any subset. The eight single-destination modes keep their existingSecondaryNavigationstrip untouched: dropping it is a deletion decision about their lone entry, not a port, and is deliberately out of scope.RAG impact: no retrieval behaviour change — navigation and layout only.
Desktop labels shorten
Specifiers and Formulation showed richer wording via their own subnavs ("Build wording", "Build formulation", "Mechanism map"); they now show the registry's "Build" and "Map".
ModeNavItemhas noshortLabelby design — a slot shows its real word or folds into More. Lengthening the registry instead was the alternative, but "Build formulation" plus "Mechanism map" do not fit the calibrated 42rem band, and ledger #113 is explicit that the thresholds are measured sums plus ~8% CI font-metric headroom, not padding to be widened.A regression this PR introduced and fixed
Removing the page-owned subnavs left
/specifiers/<slug>,/formulation/<slug>and the specifier reference pages with breadcrumbs and nothing else. The intended fallback — the shared "On this page" sections nav — silently rendered nothing, becausespecifierSections/formulationSectionsdeclared ten target ids (specifier-overview,formulation-five-ps, and eight more) that no component rendered.AvailableInformationPageNavigationfiltered every item out and returned null. The dead config predated this PR; the subnav was masking it.Fixed by wiring those ids into the pages, with binding tests per page family and a render test asserting the bar actually appears on a specifier record. Raised in review by Devin.
An invariant that was never actually stated
The header addon slot holds one page-owned header. What enforced that was an accident:
documentsanddifferentials, the only modes with a claimant, had fewer thanMODE_NAV_MIN_ITEMSdestinations, soModeNavrendered nothing. Differentials now has three.I first read
/differentials/diagnoses/<slug>as a live collision and added an explicit guard at the mode branch. Mutating it away failed no test — becausepage-secondary-navigation.tsxreturns onlocallyOwnedInformationNavigationwell before the mode branch, so the guard was unreachable. The guard is gone. What replaced it isisHeaderAddonSlotOwnedRoute, naming the claimants, plus assertions that the two separately-maintained lists actually agree route for route, and the render-level outcome. Breaking the cover now fails three assertions. When a future claimant falls outside it, that is the signal the mode branch needs a guard of its own — and the test says so.Verification
npm run verify:pr-local— stops atcheck:installed-lock-parityon a pre-existing container mismatch (playwright: installed 1.62.0 does not match locked 1.62.1), unrelated to this diff. Every remaining stage was run individually on the current tip:check:runtime—PASS: Node runtime 24.13.0/PASS: npm runtime 11.17.0format:changed—All matched files use Prettier code style!lint— clean,--max-warnings 0typecheck— cleantest—Test Files 514 passed (514)/Tests 5443 passed | 4 skipped (5447)build— completed,Client bundle secret surface check passed.check:rag:fixtures—Offline RAG fixture and manifest validation passed (36 golden cases, 23 suites).npm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changed — the wrapper aborts at the same lock-parity check, so the affected specs were run directly against Chromium:ui-mode-nav-density.spec.ts— 48 passed, including 28 new per-mode cases and two new folded-active casesui-specifiers.spec.ts,ui-formulation.spec.ts,ui-route-coverage.spec.ts— 24 passedui-phone-scroll.spec.ts,ui-phone-scroll-routes.spec.ts,ui-phone-scroll-page-owned.spec.ts,ui-chrome-scroll.spec.ts— 85 passed across two runs, includingphone differential detail navigation uses the universal collapse ownerui-accessibility.spec.ts,ui-universal-search.spec.ts— 33 passedUI verification not run: the full
verify:uiproject set and non-Chromium browsers — this container has Chromium build 1194 where playwright 1.62.1 expects 1234. CI is the authoritative browser run.New contract assertions were mutation-checked rather than assumed: removing
mapfrom the icon map fails the typecheck; droppingdifferentialsfrom the density spec table fails the coverage pin; breaking the addon-slot cover fails three assertions; removingformulationfrom the adopted list fails the membership pin. The one assertion that failed its mutation check — the original explicit slot guard — was removed rather than kept as unreachable code.Note on CI: several
Production UIandPR requiredfailures on earlier heads were GitHub Actions infrastructure (Failed to resolve action download info. Error: Service Unavailable/Bad Gateway), with no test executed, and onePR requiredwas theCANCELLED with no failing jobsupersession path after a branch sync. Those are not diff failures.Risk and rollout
ModeNavis shared — a mistake in the component lands everywhere at once. The mode-level exposure is bounded byMODE_NAV_ADOPTED_MODES. The shared component is touched once, additively:activeIdacceptsnullas an explicit "no current page", withundefinedstill deriving from the path, so Therapy's shipped behaviour is unchanged.MODE_NAV_ADOPTED_MODESto return it to the in-flow strip. For Specifiers and Formulation that also needs the subnav-removal commit reverted, since their page-owned subnavs were deleted. Reverting the navigation commits restores the pre-feat(therapy): standardise every Therapy page on the shared mode nav, centred on desktop #1642 state for these modes with Therapy untouched.Notes
/dsm,/specifiers,/formulationand/differentialsalready surface their destinations as tiles, so nothing is stranded by the removal.length < 2early return that silently deletes the submitted-search nav for the eight single-destination modes) are not carried over. Closing it is the maintainer's call.useTherapyNavItemsremains its live list.form-overview,differential-presentation-*). Pre-existing and unchanged by this PR; worth a separate audit.Summary by CodeRabbit
New Features
Bug Fixes
Tests