Skip to content

fix(ui): remove dead scroll on pages that fit the window - #2419

Merged
BigSimmo merged 1 commit into
mainfrom
claude/home-page-scroll-stability-cbnak6
Aug 27, 2026
Merged

fix(ui): remove dead scroll on pages that fit the window#2419
BigSimmo merged 1 commit into
mainfrom
claude/home-page-scroll-stability-cbnak6

Conversation

@BigSimmo

@BigSimmoBigSimmo commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Home pages and other pages whose content already ended still carried a scroll range, so a scrollbar appeared on a page with nothing below the fold and a wheel notch jolted the page into its bottom stop. Measured in Chromium across 39 routes at five viewports: 8px on every standalone page, 38–46px on all 15 shared mode homes, 57px on routes carrying the header nav row, and up to 273px on a tall window. The amount did not vary with window size, so it was present on every window, permanently.
  • The cause was the same everywhere: page-fill floors written as calc(100dvh - <chrome estimate>), and no estimate could be right. --shell-header-h (4rem) covers the header's inner min-h-14 bar plus its pb-2 but not the bar's own pt-[max(0.5rem,var(--safe-area-top))]; nothing knew about the header-collapse-addon nav row that topic routes mount; nothing knew about #main-content's own sm:pb-8; and the dashboard's 11rem guess had to cover the header block, the wrapper padding, the desktop composer slot and the space-y gap in one hard-coded number.
  • Surfaces now grow into the box above them, which is exact by construction and cannot drift again: shell #main-content grows into .phone-viewport-frame (sm:grow); mobile-composer-reserve-pad becomes the fill box at sm+ (sm:flex sm:min-h-full sm:flex-col); page shells grow into that pad (sm:grow); and the dashboard content wrapper is the fill box for the mode-home canvas, which grows into it (sm:grow sm:shrink-0).
  • Phone geometry is untouched. Below sm the document owns scrolling and there is no bounded box to fill, so the phone floors stay as they were. All 65 phone measurements are byte-identical before and after.
  • Adds the "pages that fit the window have no scroll range" guard to tests/ui-chrome-scroll.spec.ts — the assertion whose absence let this sit unnoticed.
  • Records the rule as invariant 24 in docs/search-chrome-behaviour.md and corrects the --shell-header-h comment in globals.css, which had claimed the token existed so the header height and the page-fill floors "cannot silently drift apart".
  • Updates the two contract tests that pinned the old floor strings (ui-overlay-css-contract, mobile-interaction-regressions) to pin the new fill contract, including negative assertions so the estimate cannot come back.

Verification

  • npm run verify:pr-local

Run locally instead, and reported exactly as run:

  • npm run typecheck — clean.
  • npm run lint — clean, --max-warnings 0.
  • npm run test (full offline unit suite) — 10,814 passed, 4 skipped, 3 failed. The three failures (clinical-hazard-controls, privacy-readiness-contract, rag-plan-package-parity) were confirmed pre-existing: they fail identically with this branch's src/, tests/ and docs/ changes stashed. rag-plan-package-parity fails only because the manifest's reconciled base commit is not present in this shallow clone.
  • npm run verify:phone-chrome — contracts stage passed (9 files, 135 tests passed, including header-scroll-hide-contract, use-hide-on-scroll, mobile-composer-reserve and ui-overlay-css-contract). Its browser stages could not run here, see below.
  • Browser measurement, Chromium, 39 routes × 5 viewports (390×844 phone, 700×900, 834×1112 tablet, 1440×900, 1280×1200), before and after: 82 measurements improved, 113 unchanged, 0 increased, 0 routes with horizontal overflow. Every page that fits now reports a scroll range of exactly 0; every page with real content reports the same range as before. A separate 1440×2000 pass confirmed the previously hidden cases (/favourites 40→0, /dictionary/topics 273→0, /calculators/search 224→0, /specifiers/map 273→0, /differentials/compare 273→0). Screenshots of a mode home, a standalone info page, a nav-row route, a results view and a phone home were inspected: nothing shifted, centring still reads correctly, and the background still reaches the bottom edge.
  • The four assertions the new guard makes were run directly at its exact viewport (1440×1200) against /, /?mode=documents, /medications and /factsheets/topics — all report maxScrollTop = 0 on both the settled and the stability read.

UI verification not run: npm run verify:ui and the verify:phone-chrome browser stages could not execute in this container. The lock pins Playwright chromium-1234; the image ships only chromium-1194, and both remedies are blocked by the environment's proxy — scripts/setup-claude-cloud.sh browsers fails on 403 from the apt PPAs, and npx playwright install chromium into the managed cache fails with a download error. Per the #255 guard in scripts/check-playwright-browser-revision.mjs, pointing PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH at the mismatched shell is forbidden, so browser proof is delegated to CI Production UI. The browser measurements above were taken with the container's chromium-1194 against the dev server, not the pinned revision against a production build, and are offered as evidence of the geometry change, not as the Chromium gate.

  • npm run verify:release before release or handoff confidence claims

Risk and rollout

  • Risk: Shared layout foundations. Four surfaces change from a viewport-derived min-height floor to flex growth, and two containers become flex columns at sm+ (mobile-composer-reserve-pad, the dashboard content wrapper). The realistic failure mode is a page that no longer stretches to the bottom — visually neutral where it happens, because the shell and every one of these surfaces paint the same var(--background), but worth a look on wide layouts. Percentage resolution is one level deep in both chains (min-h-full against a parent with a definite height), which is why the nested favourites grid was moved to sm:min-h-full against its now-grown hub rather than left on a viewport calc.
  • Rollback: Single commit, revert cleanly. No data, schema, or configuration change.
  • Provider or production effects: None. No Supabase, OpenAI, CI, or deployment surface touched; no provider-backed command was run.
  • RAG impact: none — no retrieval, ranking, selection, or answer surface is touched.

Notes

  • Two residuals were measured and deliberately left, both outside this change's cause:
    • /calculators/search at 1280×1200 keeps a 2px range. That is the catalogue grid's own content being 2px taller than its space, not a chrome estimate; no floor binds there any more.
    • mode-home-page-skeleton.tsx still carries sm:min-h-[calc(100dvh-var(--shell-header-h))]. It is a route loading.tsx placeholder whose parent chain is a Suspense boundary rather than the reserve pad, so growth would not resolve there. It is transient and cannot produce the persistent scrollbar this PR fixes.
  • Two console messages seen while testing are pre-existing and unrelated — a getServerSnapshot should be cached warning on /favourites and a 404 for one resource on /. Both reproduce identically with this branch's changes stashed.
  • The --shell-header-h token keeps its value and its other consumers (chrome offsets, --phone-overlay-chrome-h, which adds the top pad back explicitly). Only its use as a page-fill floor is retired.

Generated by Claude Code


Note

Medium Risk
Touches shared shell flex geometry (#main-content, reserve pad, dashboard wrapper); failure mode is pages not stretching to the bottom on wide layouts, though backgrounds should mask it.

Overview
Fixes phantom scrollbars on desktop/tablet (sm+) when page content already fits the viewport. Floors built as calc(100dvh - <chrome estimate>) systematically overshot because no static number could account for the header’s top safe-area pad, optional header-collapse-addon nav row, or #main-content’s bottom padding — leaving roughly 8–273px of empty scroll range.

Layout contract change: at sm+, #main-content and the mobile-composer-reserve-pad become a definite flex fill chain (sm:grow, sm:flex sm:min-h-full sm:flex-col); page shells, dashboard content wrapper, mode-home canvas, and shared primitives (searchPageShell, InformationPageShell, therapy/differentials/favourites mains) switch from viewport min-h calcs to sm:grow (favourites split rail uses sm:min-h-full against the grown hub). Phone viewport calcs are unchanged where the document still owns scrolling.

Documents invariant 24, clarifies --shell-header-h is for offsets—not page fill—and adds Playwright “pages that fit the window have no scroll range” plus contract-test pins so the old estimate pattern cannot return.

Reviewed by Cursor Bugbot for commit c56e5f5. Configure here.

Pages whose content already ended still carried a scroll range, so a
scrollbar appeared on a page with nothing below the fold and a wheel
notch jolted the page into its bottom stop. Measured in Chromium across
39 routes at five viewports: 8px on every standalone page, 38-46px on
all 15 shared mode homes, 57px on routes carrying the header nav row,
and up to 273px on a tall window.
The cause was the same everywhere: page-fill floors written as
`calc(100dvh - <chrome estimate>)`. No estimate could be right.
`--shell-header-h` (4rem) covers the header's inner bar plus `pb-2` but
not the bar's own `pt-[max(0.5rem,var(--safe-area-top))]`; nothing knew
about the `header-collapse-addon` nav row on topic routes; nothing knew
about `#main-content`'s own `sm:pb-8`; and the dashboard's `11rem`
guess had to cover the header block, the wrapper padding, the desktop
composer slot and the space-y gap in one number.
Surfaces now grow into the box above them instead, which is exact by
construction and cannot drift again:
- shell `#main-content` grows into `.phone-viewport-frame` (`sm:grow`)
- `mobile-composer-reserve-pad` becomes the fill box at sm+
- page shells grow into that pad (`sm:grow`)
- the dashboard content wrapper is the fill box for the mode-home
canvas, which grows into it (`sm:grow sm:shrink-0`)
Phone geometry is untouched: below `sm` the document owns scrolling and
there is no bounded box to fill. All 65 phone measurements are
byte-identical before and after.
Verified in Chromium against 39 routes x 5 viewports: 82 measurements
improved, 113 unchanged, 0 increased, and no route gained horizontal
overflow. Every page that fits now reports a scroll range of exactly 0;
every page with real content reports the same range as before.
Adds the "pages that fit the window have no scroll range" guard to
tests/ui-chrome-scroll.spec.ts, records the rule as invariant 24 in
docs/search-chrome-behaviour.md, and updates the two contract tests
that pinned the old floors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bbm7tQoyg6etyf7NqoA65d
@coderabbitai

coderabbitaiBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 54 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 100 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 749a6696-5130-4e40-afa9-f24539d38706

📥 Commits

Reviewing files that changed from the base of the PR and between 3265231 and c56e5f5.

⛔ Files ignored due to path filters (1)
  • docs/search-chrome-behaviour.md is excluded by !docs/**, !**/*.md
📒 Files selected for processing (15)
  • src/app/globals.css
  • src/components/ClinicalDashboard.tsx
  • src/components/clinical-dashboard/favourites-command-library-page.tsx
  • src/components/clinical-dashboard/global-search-shell.tsx
  • src/components/clinical-dashboard/mode-home-canvas.ts
  • src/components/differentials/differential-compare-queue-page.tsx
  • src/components/differentials/differential-presentation-workflow-page.tsx
  • src/components/differentials/differential-stream-workspace.tsx
  • src/components/information-page-shell.tsx
  • src/components/mode-home-template.tsx
  • src/components/therapy-compass/workspace.tsx
  • src/components/ui-primitives.tsx
  • tests/mobile-interaction-regressions.test.ts
  • tests/ui-chrome-scroll.spec.ts
  • tests/ui-overlay-css-contract.test.ts

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

@supabase

supabaseBot commented Aug 27, 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 ↗︎.

@BigSimmo
BigSimmo marked this pull request as ready for review August 27, 2026 10:10
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@cursor

cursorBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_c9e42d1a-952c-4095-ba98-cf546141ce2f)

@BigSimmo
BigSimmo merged commit 603da96 into mainAug 27, 2026
55 of 57 checks passed
@BigSimmo
BigSimmo deleted the claude/home-page-scroll-stability-cbnak6 branch August 27, 2026 10:27
BigSimmo pushed a commit that referenced this pull request Aug 27, 2026
The scroll work landed on main as 603da96; this branch still carries the
three phone fixes that were pushed after that merge. Merging main in makes
those the only diff a new pull request will show.
BigSimmo added a commit that referenced this pull request Aug 27, 2026
Apply #2419's sm:grow page-shell change on the moved searchPageShell recipes so the PR is no longer CONFLICTING.
Co-authored-by: Cursor <cursoragent@cursor.com>
BigSimmo added a commit that referenced this pull request Aug 27, 2026
#2434)
* issues: queue three follow-ups left open by PR #2422
Three append-only intake requests, plus the regenerated snapshot that
check:outstanding-issues requires once the inbox is non-empty.
- P2 issue: Lighthouse desktop-root LCP read 961/925/900/852 on the #2422
branch against 748/705 on main, same 786ms baseline. It failed the gate
once and cost a re-run. Not root-caused; recorded with the evidence on
both sides, the one mechanism still testable (whether extracting
answer-cancelled-notice.tsx added a chunk to /), the exact command to
test it, and the caveat that the gate's 3-sample majority is taken on a
single runner and so cannot see runner-level slowness.
- P3 issue: /calculators/search keeps a 2px residual scroll range at
1280x1200, deliberately out of scope for #2419 because it is real
content rather than the chrome-estimate floor that PR removed.
- P3 task: mode-home-page-skeleton.tsx:32,60 still subtract
--shell-header-h from 100dvh - the last page-fill estimates in src/ and
the pattern invariant 24 retired. Left because the Suspense parent chain
has to carry a definite height first.
Requests are immutable and merge independently; reconciliation is a
separate serial branch and is deliberately not done here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bbm7tQoyg6etyf7NqoA65d
* ledger: record the immutable review for the #2422 branch
Owed from the handoff and deliberately not pushed onto the branch while
it was green and armed for auto-merge.
Records the scope, the merge outcome verified by content rather than by
the green tick (git diff c917e0728b1fef is empty, so the squash dropped
nothing), the CI evidence on the merged head, and two honest gaps: the
Chromium gate could not run in this container under the #255 revision
pin, and the desktop-root LCP reading is carried as its own open item
rather than reported as cleared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bbm7tQoyg6etyf7NqoA65d
* snapshot: regenerate repo-awareness after the new review record
`Static PR checks` failed on this branch: check:repo-awareness-snapshot
reported "review_state differs from the repository". The repo-awareness
snapshot mirrors the branch-review records, so appending one leaves it
behind; I regenerated the outstanding-issues snapshot in the first commit
and missed this second one.
Regenerated with the command the check itself names. The diff is only the
captured revision, the mirrored copy of the new record, and counts
2644 -> 2645 records / 1612 -> 1613 refs. Nothing unrelated moved.
Verified: check:repo-awareness-snapshot now reports "in step ... (195
pages, 481 documents, 2645 reviews)"; the failure reproduced locally
first. Also re-ran the rest of the failing job's docs and ledger gates
plus the whole-tree format check — docs:check-index, docs:check-inventory,
docs:check-scripts, docs:check-links, check:outstanding-issues,
check:ledger-write-discipline, check:branch-review-ledger, format:check —
all pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bbm7tQoyg6etyf7NqoA65d
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto 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.

2 participants

@BigSimmo@claude