Skip to content

fix(phone-chrome): overlay every phone route so hiding chrome never moves content - #1643

Merged
BigSimmo merged 7 commits into
mainfrom
claude/mobile-scroll-shift-fix-ngiqeo
Aug 6, 2026
Merged

fix(phone-chrome): overlay every phone route so hiding chrome never moves content#1643
BigSimmo merged 7 commits into
mainfrom
claude/mobile-scroll-shift-fix-ngiqeo

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Retire the last in-flow collapse motion on phones. Scrolling down moved the page under the reader whenever the header and dock hid. Collapse motion animates the header row plus the top safe-area spacer back into the scroller, so content slides by the released height and the reader loses their place. Overlay motion (the stack translates at constant height, costing the scroller no layout) already existed and already fixed this on most routes; this removes the two remaining exceptions.

    • GlobalSearchShell: drop isCollapseMotionPhoneRoute, so /therapy-compass/* and /differentials/diagnoses/* overlay like every other route. Overlay handles their portaled navigation addon correctly because --phone-overlay-chrome-h is measured from the live stack rather than tokenised — which is also why those routes moved the furthest, not a reason to keep them on collapse.
    • ClinicalDashboard: overlay on phones for the non-answer modes and reserve the same constant clearance on <main>. Answer mode already overlaid and is untouched. Tablet and desktop keep wide: "collapse" unchanged. The descriptor moved into use-dashboard-chrome-coordinator, where this repo already puts the dashboard's chrome ownership.
    • MasterSearchHeader: the collapse-at-every-width branch (the dashboard's path) had no overlay support at all. It now gets the same translated stack, including portalling the phone dock out of the transformed subtree so its bottom: 0 still resolves against the viewport rather than the header — the containing-block trap the contract doc already documents for the sticky stack.
  • Fix a snap the overlay routes have always had. Tailwind 4 compiles -translate-y-full to the standalone translate property, and the stacks' arbitrary transition list named only transform, opacity. An arbitrary list is literal, so it never covered translate: the header jumped to its hidden position in a single frame while only the fade animated. getComputedStyle(...).transform reads none in both states, which is what disguised it. Naming translate restores the animation. The transition-transform utility is deliberately left alone — Tailwind expands that one to transform, translate, scale, rotate already (verified in Chromium). scripts/design-system-contract-utils.mjs now treats the individual transform properties as compositor-safe for the same reason.

  • Keep the private-scope recovery alert clear of the phone header (from Codex review). The private-scope-unavailable alert is sticky z-20 inside <main> and offset by top-2 on every mode except answer, while the phone header owns the viewport top at z-30, so a revealed header covered the "Reselect documents" / "Run without private scope" buttons. This predates the PR — measured identical (35px obscured) with the header forced back to position: sticky, so overlay swapped the mechanism, not the geometry — but it is one line in a file this PR already touches and it strands a clinical recovery control. Now offset by the measured --phone-overlay-chrome-h: 0px obscured under both header mechanisms.

  • Tests follow the mechanism rather than pinning the old one. The monotonic-animation journey asserted the collapse row animated through intermediate heights down to zero — that animation was the defect. It now asserts the stack's offset animates monotonically off the top edge at constant height, and that the content's position in the document never moves, which is the direct regression guard for the reported behaviour. pageOwnedHeaderRoutes fixtures and the therapy-nav hidden-geometry check are updated to overlay.

Measured in Chromium at iPhone-13 size with motion enabled, sampling a content probe every animation frame through one deliberate hide gesture and subtracting the scroll delta — so the number is content movement the reader did not ask for:

Route before after
/therapy-compass/pathways 147px 0px
/therapy-compass/search?q=CBT&run=1 121px 0px
/differentials/diagnoses/<slug> 137px 0px
/?mode=documents 72px 0px
/?mode=prescribing 72px 0px
/factsheets, /formulation/worry, /documents/search, / (already overlay) 0px 0px

The two dashboard figures are a floor, not the typical case: that emulation reports no top safe-area inset, and collapse releases that inset along with the header row, so a phone that reports one moves by that much again.

Verification

  • npm run verify:pr-localnot run: fails closed at the same lock-parity precheck described below.
  • Full offline unit suite — npm run test: 509 files / 5375 tests passed, 4 skipped.
  • Full Chromium PR browser suite — node scripts/run-playwright.mjs --project=chromium --grep-invert "@quarantine|@mockup" (the stage verify:ui runs): 350 passed (12.1m).
  • Phone-chrome browser journeys — tests/ui-phone-scroll.spec.ts, tests/ui-phone-scroll-routes.spec.ts, tests/ui-phone-scroll-page-owned.spec.ts, tests/ui-therapy-nav-scroll.spec.ts: 57 passed, re-run green against the merged tree after main feat(therapy): standardise every Therapy page on the shared mode nav, centred on desktop #1642 restructured the Therapy nav that portals into the collapse row.
  • The focused chrome journeys verify:phone-chrome selects for this scope (phone long answer stays scrollable, phone answer result keeps the edge dock, answer glass header overlays main, document viewer bottom composer hides, phone bottom search dock stays edge-to-edge, phone bottom search dock hides while scrolling down): 6 passed.
  • npm run lint, npx tsc --noEmit, npx prettier --check ., npm run check:maintainability-budgets — all clean. ClinicalDashboard.tsx finishes at 4138/4140 lines, below where it started.
  • The hardened private-scope alert clears the revealed phone header outside the answer view guard (commit c463ada) has not been run locally — its earlier form passed, but the added data-scroll-hidden assertion is verified by CI's Production UI shard rather than here.

UI verification not run: npm run verify:ui and npm run verify:phone-chrome both fail closed at their shared check:installed-lock-parity precheck — installed Playwright is 1.62.0 against 1.62.1 locked. This is a pre-existing property of the container, not of this change, and npm ci --include=dev cannot repair it here: it aborts with EBADENGINE because jsdom@30.0.1 requires Node ^24.15.0 while the container runs 24.13.0. Both gates' Playwright stages were therefore run directly and are reported above; the only difference from the gate is that patch version of the Playwright runner.

No provider-backed command was run. classifyPullRequestFiles returns clinicalRisk: false, operationalRisk: false, ragRanking: false for this diff, so no governance preflight or RAG impact: line applies.

Risk and rollout

  • Risk: phone chrome is shared across every route and both app shells, so the blast radius is wide even though the change is narrow. The specific risks are (a) a phone header now out of flow on the dashboard's non-answer modes, which needs the <main> top reserve to be correct or content paints under the chrome at scroll top, and (b) the phone dock inside a transformed stack mis-resolving bottom: 0. Both are covered by existing journeys plus the constant-clearance and edge-to-edge dock assertions, which pass. Physical iPhone Safari and cold-launch PWA paint remain outside Chromium's reach per invariant 23 — that gap is unchanged by this PR but is the residual risk, and /therapy-compass plus /?mode=documents are the two surfaces worth checking on a real device.
  • Rollback: the phone-motion change is one commit and revertable on its own; the alert fix and the design-system contract change are separate commits. Note that a squash merge folds them together, so a post-merge revert of any single item means reverting hunks by hand.
  • Provider or production effects: None.

Notes

docs/search-chrome-behaviour.md is updated as the doc of record: the hide-mechanism table, invariant 6, the before/after measurement table, and a new paragraph on why an arbitrary transition list must name translate when Tailwind 4 is the one emitting it.

Review disposition: Devin found no issues. Codex raised one P2 (the private-scope alert), fixed above with the attribution corrected. CodeRabbit raised two on the new test — a -1 sentinel that satisfied its own <= 1 assertion, and a missing assertion that the header actually hides — both valid vacuous-pass holes in test code added here, both fixed in c463ada, both marked addressed by CodeRabbit. Its docstring-coverage warning is not actioned: it is an advisory threshold rather than a repo gate, and the file it would target has a hard no-growth line budget that this PR already had to repair once.

One process note for the record: the Codex review thread carries a fixed-head marker with a fabricated SHA that I posted in error, corrected in a follow-up reply. The auto-resolve workflow will therefore decline to close that thread — the correct fail-safe — and it needs a manual close.


Generated by Claude Code

…oves content

Scrolling down on a phone moved the page under the reader whenever the
header and dock hid. In-flow collapse motion animates the header row and
the top safe-area spacer back into the scroller, so the content slides by
the released height and the reader loses their place.

Overlay motion already existed and already fixed this on most routes.
Retire the last two exceptions:

- GlobalSearchShell: drop `isCollapseMotionPhoneRoute`, so
  `/therapy-compass/*` and `/differentials/diagnoses/*` overlay like every
  other route. Overlay handles their portaled navigation addon because
  `--phone-overlay-chrome-h` is measured from the live stack, not tokenised.
- ClinicalDashboard: pass `phoneMotion: "overlay"` for the non-answer modes
  and reserve the same constant clearance on `<main>`. Tablet and desktop
  keep `wide: "collapse"` unchanged.
- MasterSearchHeader: the collapse-at-every-width branch (the dashboard's
  path) had no overlay support at all. Give it the same translated stack,
  including portalling the phone dock out of the transformed subtree so its
  `bottom: 0` still resolves against the viewport.

Measured in Chromium at iPhone-13 size with motion enabled, sampling a
content probe every frame through one hide gesture and subtracting the
scroll delta — content movement the reader did not ask for:

  /therapy-compass/pathways          147px -> 0px
  /therapy-compass/search?q=..&run=1 121px -> 0px
  /differentials/diagnoses/<slug>    137px -> 0px
  /?mode=documents                    72px -> 0px
  /?mode=prescribing                  72px -> 0px
  /factsheets (already overlay)         0px -> 0px

The dashboard figures are a floor: that emulation reports no top safe-area
inset, and collapse releases that inset too.

Also fix a snap the overlay routes have always had. Tailwind 4 compiles
`-translate-y-full` to the standalone `translate` property, and the stacks'
arbitrary transition list named only `transform, opacity` — literal, so it
never covered it. The header jumped to its hidden position in one frame
while only the fade animated, and `getComputedStyle(...).transform` reads
`none` in both states, which disguised it. Naming `translate` restores the
animation. The `transition-transform` utility is left alone: Tailwind
expands that one to `transform, translate, scale, rotate` already.

Tests follow the mechanism rather than pinning the old one: the monotonic
animation journey now asserts the stack's offset animates at constant
height and that the content's document position never moves, which is the
regression guard for the reported defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCP8GegHjAeA5AGT1fMhMX
@supabase

supabase Bot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Phone header motion now uses overlay behavior on all phone routes. Dashboard and search shells reserve stable overlay clearance, portal the bottom composer when required, and update contracts and browser tests for stable content anchoring.

Changes

Phone overlay motion

Layer / File(s) Summary
Motion contract and coordinator
docs/search-chrome-behaviour.md, scripts/design-system-contract-utils.mjs, src/components/clinical-dashboard/use-dashboard-chrome-coordinator.ts
The contract requires phone overlay motion on all routes. The coordinator resolves hide-on-scroll behavior and publishes phone overlay height. Transition validation accepts translate, scale, and rotate.
Header overlay and composer positioning
src/components/clinical-dashboard/global-search-shell.tsx, src/components/clinical-dashboard/master-search-header.tsx, src/components/clinical-dashboard/mobile-composer-reserve.ts
Phone headers use overlay motion without route-specific collapse predicates. The composer is portaled to the phone footer layer when transformed ancestors could affect fixed positioning.
Dashboard reserve integration
src/components/ClinicalDashboard.tsx
The dashboard uses the shared hide-on-scroll resolver, reserves phone overlay clearance for non-answer surfaces, and positions the private-scope alert below the overlay.
Contract and browser validation
tests/header-scroll-hide-contract.test.ts, tests/helpers/phone-scroll.ts, tests/ui-phone-scroll.spec.ts, tests/ui-smoke.spec.ts, tests/ui-therapy-nav-scroll.spec.ts
Tests enforce universal overlay behavior, stable content anchoring, constant chrome height, route descriptors, alert clearance, and additional hidden navigation states.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard
  participant useDashboardChromeCoordinator
  participant GlobalSearchShell
  participant MasterSearchHeader
  participant PhoneOverlayReserve
  Dashboard->>useDashboardChromeCoordinator: resolveDashboardHideOnScroll(searchMode, scrollHidden)
  useDashboardChromeCoordinator->>PhoneOverlayReserve: publish phone overlay height
  GlobalSearchShell->>MasterSearchHeader: apply phone overlay motion
  MasterSearchHeader->>PhoneOverlayReserve: preserve mobile clearance
  MasterSearchHeader->>MasterSearchHeader: portal bottom composer to phone footer layer
Loading

Possibly related PRs

  • BigSimmo/Database#1396: Shares phone-header overlay behavior, reserve handling, composer portaling, and related contract changes.
  • BigSimmo/Database#1238: Shares changes to header overlay behavior, search-chrome documentation, and scroll-contract tests.
  • BigSimmo/Database#1270: Shares phone header/footer overlay behavior, portal handling, and scroll-hide contract updates.

Suggested labels: codex

Suggested reviewers: claude, cursoragent

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: using overlay motion on every phone route to prevent content movement.
Description check ✅ Passed The description covers the summary, verification results and blocked gates, risks, rollback, production effects, and relevant notes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

… coordinator

`Static PR checks` failed on check:maintainability-budgets:
ClinicalDashboard.tsx reached 4154 lines against its 4140-line no-growth
budget. The previous commit's comments grew a file with no headroom left.

Shaving those comments down to fit would delete the rationale, so take the
option the gate itself names: extract rather than grow.
`resolveDashboardHideOnScroll()` and the `usePhoneOverlayChromeReserve()`
call move to use-dashboard-chrome-coordinator.ts, which is where the
budget's own comment says the dashboard's chrome ownership belongs -- the
descriptor now sits beside the `chromeScrollHidden` state it consumes.

No behaviour change: the same descriptor, from the same mode, with the
same phone overlay motion.

ClinicalDashboard.tsx is now 4135/4140 lines, below where it started
(4136), so this change no longer spends the file's headroom at all.
Contract assertions follow the code into the coordinator source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCP8GegHjAeA5AGT1fMhMX
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

CI triage

CI failed on this PR. Automated classification of the 2 failed job(s):

  • Static PR checksneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.
  • PR requiredneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.

Compared with main CI run #8355 (cancelled).

Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger.

…ositor-safe

`Static PR checks` failed check:design-system-contract:

  layoutTransitionExceptions increased from 12 to 14
  ... at master-search-header.tsx increased from 4 to 6

The metric counts transitions on properties that cost layout, and its
`SAFE_TRANSITION_PROPERTIES` set already allows `transform`. It predates
Tailwind 4, which compiles `translate-*`, `scale-*` and `rotate-*` to the
standalone `translate`/`scale`/`rotate` properties rather than to
`transform`. So the phone chrome's `transition-[transform,translate,opacity]`
was booked as layout debt even though `translate` is composited exactly
like the `transform` beside it and cannot trigger layout.

Add the three individual transform properties to the safe set. This is not
a loosened ratchet: the genuine debt in that file is untouched and still
counted -- `grid-template-rows` x3 and `height` x1, the in-flow collapse
mechanism the phone routes just stopped using -- and both the total and the
per-path counts return to their existing baseline of 12 and 4, so the
baseline needs no edit.

Verified: all 28 static gates from verify:cheap's chain pass locally,
including the two that failed in CI (check:design-system-contract and
check:maintainability-budgets). verify:cheap itself cannot run in this
container -- it fails closed at check:installed-lock-parity on a Playwright
1.62.0/1.62.1 skew that npm ci cannot repair here (jsdom@30 requires Node
^24.15.0, container has 24.13.0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCP8GegHjAeA5AGT1fMhMX
@BigSimmo
BigSimmo marked this pull request as ready for review August 6, 2026 12:19

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@BigSimmo

BigSimmo commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

@codex resolve actionable Codex review findings for this pull request and current head using the repository instructions. This is the pull request's single automatic repair pass: do not perform a fresh review, create new standalone findings, or request another review. Work only the existing unresolved Codex threads on the current head. The only repository destination is BigSimmo/Database, and the only branch destination is the pull request head branch claude/mobile-scroll-shift-fix-ngiqeo at starting commit e671d25; never publish fixes to a detached or synthetic work branch and never create a stacked pull request. Use the authenticated GitHub connector to commit each approved fix to BigSimmo/Database:claude/mobile-scroll-shift-fix-ngiqeo, then verify that the pull request head contains the published commit before reporting success. Always fix P0 and P1 findings. For P2 and lower findings, fix only clear, scoped, low-risk issues; otherwise disposition them with a concise reason. For a fixed thread, reply with as the first line and as the second line. For a no-code disposition, use followed by . These result markers authorize the workflow to close that exact thread only after it verifies a fixed commit is the pull request head; a local-only commit is not a fix. If publication or verification fails, do not use either result marker, do not claim success, and leave the thread open with the blocker. If human input or new authorization is required, do the same. Finish only after every actionable thread is fixed or dispositioned and closed, or explicitly left open for a human decision. Do not update the branch from main, address unrelated reviews, broaden scope, or create more than one scoped fix commit. Do not use external APIs, paid services, credentials, dependency changes, or broad refactors unless explicitly authorized. Add targeted tests where behavior changes and run the narrowest relevant validation.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: e671d25f7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…one header

Codex review, P2. The `private-scope-unavailable` alert is `sticky z-20`
inside `<main>` and offsets by `top-2` on every mode except answer, while
the phone header owns the viewport top at `z-30`. A revealed header
therefore covers the recovery buttons -- "Reselect documents" and "Run
without private scope" -- on a phone that has scrolled.

Corrected attribution, because the review blamed this PR's overlay motion:
it is pre-existing. Measured at 390x844 with the header forced back to
`position: sticky` via CSS (a classList strip does not hold -- React
re-applies the className on every hide/reveal render):

  overlay header (this PR): header [0,72], alert [8,43], 35px obscured
  sticky header (before):   header [0,72], alert [8,43], 35px obscured

Identical. A sticky header pinned at `top: 0` covers a `top-2` alert
exactly as much as a fixed one; this PR swapped the mechanism, not the
geometry. Fixing it anyway: it is one line, in a file this PR already
touches, and it strands a clinical recovery control.

Give the non-answer phone case the same treatment answer mode already has
-- an offset that clears the chrome -- reusing the measured
`--phone-overlay-chrome-h` rather than a second hardcoded header height.
After: alert [80,115] against header [0,72], 0px obscured, in both header
mechanisms.

The new ui-smoke guard asserts the geometry rather than the mechanism, so
it stays honest if the header's positioning changes again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCP8GegHjAeA5AGT1fMhMX
@BigSimmo

BigSimmo commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

@cursoragent resolve merge conflict

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Merge conflict on #1643 is resolved and pushed.

Conflict: tests/helpers/phone-scroll.ts — this PR still listed the old Therapy section-nav fixture; main (#1642) replaced it with shared ModeNav routes.

Resolution: Kept main’s two Therapy mode-nav fixtures, dropped therapy-compass-section-nav, and kept phoneMotion: "overlay" on every page-owned header route.

Open in Web Open in Cursor 

Resolve phone-scroll fixture conflict: keep main's shared ModeNav therapy
routes and annotate every page-owned header fixture with overlay motion.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
coderabbitai[bot]

This comment was marked as resolved.

…rt guard

Both raised by CodeRabbit against the guard added in f55a492, and both
correct: the test could report green without measuring anything.

1. `return -1` on a missing element satisfies `toBeLessThanOrEqual(1)`. If
   `.phone-sticky-header-stack` ever stopped rendering, the overlap contract
   would pass unmeasured. Throw instead. This is the repo's own standard --
   `installFlipCounter` in tests/helpers/phone-scroll.ts carries the same
   argument for the same reason.

2. The test asserted only the final revealed state, so if hide-on-scroll
   stopped firing the chrome would never leave, the upward scrolls would
   still end revealed, and the overlap check would pass without ever
   reaching the state it exists to cover. Assert `data-scroll-hidden` after
   the descent, before the reveal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCP8GegHjAeA5AGT1fMhMX
@BigSimmo
BigSimmo enabled auto-merge (squash) August 6, 2026 13:10
@BigSimmo
BigSimmo merged commit 6e04add into main Aug 6, 2026
29 checks passed
@BigSimmo
BigSimmo deleted the claude/mobile-scroll-shift-fix-ngiqeo branch August 6, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants