Repository navigation
fix(client): scroll inside inline page modals again, not the document - #5148
Conversation
#5009 unbounded the layout height chain so the homepage scrolls at the document level (needed for ad viewability). That also unbounded the inline page modals rendered in the content area (store, settings, leaderboard, help, ...): they used to get a bounded height from `body h-full overflow-hidden` -> content div -> `main-layout`, so o-modal's `[data-modal-scroll]` container did the scrolling. Without it the modal grew to its full content height and the whole page scrolled. Re-lock the layout only while an inline page is open, keyed off a `page-open` body class toggled in showPage() (same pattern as `in-game`). body becomes a column there because #5009 made it a wrapping row to give the Playwire header-ad slot its own line — a column lets the content area take the height remaining under that slot instead of overflowing by its height. The `min-h-0` additions restore what the removed overflow rules implicitly gave those flex children; they are inert on the homepage, whose chain is auto-height. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review. WalkthroughInline page navigation now applies ChangesInline page layout
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This change restores scrolling inside inline page modals while preserving document-level scrolling on the homepage. No actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🤖 Claude Code ReviewVerdict: The core mechanism is sound, but there's a real re-entrancy bug that undoes the fix on the most common navigation path. Findings: 1 High, 0 Medium, 0 Low. High
The new toggle runs early in window.currentPageId = pageId; // Navigation.ts:17
document.body.classList.toggle("page-open", pageId !== "page-play"); // Navigation.ts:19 (new)
...
const visibleModal = document.querySelector(".page-content:not(.hidden)"); // Navigation.ts:24ish
if (visibleModal) {
if (... visibleModal.isOpen() ...) {
visibleModal.close(); // closes the modal that was open
}
...
}For // src/client/components/BaseModal.ts:238-242
if (this.inline) {
this.style.pointerEvents = "none";
if (window.showPage) {
window.showPage?.("page-play");
}
}Trace for "page A (inline modal) is open, user navigates to page B (a different inline modal)" — e.g. clicking a nav-bar item while Store/Settings/Leaderboard/etc. is open, or
End state: page B is visible, but This isn't a rare edge case — it fires whenever a user switches between page modals without returning to the play page first (very common via the nav bar), and whenever Suggested fix: move the |
showPage() set the page-open body class before closing the previously
visible modal. An inline modal's close() re-enters showPage("page-play"),
which cleared the class again, and the outer call never restored it —
so navigating Store -> Settings (nav bar, ModalRouter) still scrolled the
document instead of the modal. Toggle the class after the close block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🤖 Claude Code ReviewVerdict: No issues found — approve. Findings: 0 critical, 0 high, 0 medium, 0 low. Reviewed No issues found. Checked for bugs and CLAUDE.md compliance. |
…#5148) Regression fix for #5009 (`7ae09256b`). No approved issue — reporting and fixing in one go. ## Description: Since #5009, opening an inline page modal (Store, Settings, Leaderboard, Help, ...) scrolls the **whole document** instead of scrolling inside the modal. **Cause.** Those modals are the `.page-content` elements in `index.html`, sized `w-full h-full`. They used to get a bounded height from the locked chain #5009 removed: `body h-full overflow-hidden` → content div `h-full overflow-hidden` → `main-layout`'s `overflow-y-auto`. Unbounding that chain is correct for the homepage (document-level scrolling, needed for ad viewability), but it also let the page modals grow to their full content height, so `o-modal`'s `[data-modal-scroll]` container had nothing left to scroll. Measured at `main` with Help open: document `scrollHeight` 7353px, modal container `clientHeight == scrollHeight == 7034`. **Fix.** Re-lock the layout *only* while an inline page is open, keyed off a `page-open` body class — the same pattern as the existing `in-game` class: - `src/client/Navigation.ts` — `showPage()` toggles `body.page-open`. It's the single place page visibility changes; `BaseModal.open()/close()` for inline modals route through it. - `src/client/styles.css` — `body.page-open { height: 100%; overflow: hidden; flex-direction: column; flex-wrap: nowrap }`. The column matters: #5009 made body a wrapping flex row so the Playwire header-ad slot (`#pw-oop-flex_container`) gets its own full-width line. A column lets the content area take the height *remaining* under that slot rather than overflowing by its height. - `index.html` — content area gets `in-[.page-open]:min-h-0 in-[.page-open]:overflow-hidden`; the ad slot gets `flex-basis: auto; flex-shrink: 0` under `body.page-open`, since its usual `flex-basis: 100%` would mean 100% *height* in a column. - `src/client/components/MainLayout.ts` — `min-h-0` on `main` and the inner column so they can shrink to the bounded height. This is what the removed `overflow-hidden` / `overflow-y-auto` implicitly gave them; inert on the homepage, whose chain is auto-height. The homepage keeps scrolling at the document level exactly as #5009 intends — the class is only present while a page modal is open. ## Verification Headless Chromium against the dev server, at 1400×900 and 390×844: - **Repro at `main`:** Help open → document `scrollHeight` 7353 / 10470, `window.scrollTo(0, 5000)` moves the page, modal container doesn't scroll. - **With this PR:** Help open → `scrollHeight == innerHeight` (900 / 844), `window.scrollTo` doesn't move, modal container scrolls (7034px of content in a 581px box; 10284 in 658 on mobile), footer pinned at the viewport bottom. - **Back to Play:** class removed, document scrolls again. - **Header ad interaction:** simulated a 250px `#pw-oop-flex` unit in its slot with a page open → slot occupies 0–250, nav sits at 250, the modal's scroll box shrinks to 331px and still scrolls, footer still at the viewport bottom, no document scroll. - 10 navigation-related client test files (72 tests) pass; Prettier + ESLint clean. Not covered: the real Playwire creative (RAMP won't initialize headless). The simulation above reproduces the geometry #5009 describes, but a headed pass with ads live is worth a glance. ## Please complete the following: - [x] I have added screenshots for all UI updates — no visual change; the fix is scroll behavior, which the measurements above capture - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file — N/A, no user-visible text - [x] I have added relevant tests to the test directory — N/A, pure CSS/layout behavior; verified e2e as above ## Please put your Discord username so you can be contacted if a bug or regression is found: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ad (#5243) Regression surfaced by #5009 / #5237 (the flex header ad). No approved issue — reporting and fixing in one go. ## Description: With the Playwire flex header ad on the page, opening any modal (Help, Store, Settings, …) rendered it **unstyled and unscrollable**. **Cause.** `o-modal` is shadow DOM and gets the page's Tailwind by copying the document's stylesheets into a constructed sheet (`SharedStyles.ts`). In dev, `Main.ts` imports the components ahead of `styles.css`, so the first copy runs before Vite has injected the `<style>` tags (~18 rules), and the module re-read on `window` **`load`**. The GumGum creative keeps an iframe loading indefinitely, so `load` never fires — measured `document.readyState === "interactive"` 65s+ after the unit rendered. The re-read never happened, so the modal had no `overflow-y-auto` / `flex-1` / `min-h-0`, grew to its full content height inside the clipped page (`[data-modal-scroll]` computed `overflow-y: visible`, `clientHeight == scrollHeight`), and could not scroll. **Fix.** Re-read on `DOMContentLoaded` instead. It fires right after the deferred module scripts finish executing (verified there is no top-level `await` in the client graph), and ad iframes cannot hold it up. Prod is unaffected either way: the `<link rel="stylesheet">` is already in `<head>` at module-evaluation time, so the initial copy works there and this path is dev-only insurance — but the `load` dependency was a footgun regardless. Not touched: `SharedStyles` also copies whatever `<style>`/`<link>` ad scripts have injected by the time it runs (`#pwCSSstyling`, the Playwire config stylesheet). That was already the case on `load`; with `DOMContentLoaded` it now runs *before* those exist, which is if anything cleaner. ## Verification Headed Chromium against `npm run dev` with the real creative (GAM preview URL + Playwire requesting the `flex` unit), 1400×900: - **Before:** Help open under the ad → shadow sheet has 18 rules, no `.overflow-y-auto`; scroll box `overflow-y: visible`, 6463px tall; wheel does nothing. - **After:** shadow sheet fully populated; scroll box `overflow-y: auto`, 259px tall with 6704px of content; wheel moves `scrollTop` 0 → 1500, document stays put. Store page scrolls too. - **No ad** (headless, desktop 1400×900 + mobile 390×844): unchanged — Help scrolls inside the modal, document locked, as after #5148. - Prettier + ESLint clean. ## Please complete the following: - [x] I have added screenshots for all UI updates — no visual change; the fix restores the modal's existing styling - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file — N/A - [x] I have added relevant tests to the test directory — N/A, depends on the live Playwire script / document load timing; verified e2e as above ## Please put your Discord username so you can be contacted if a bug or regression is found: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Code <noreply@anthropic.com>
…ad (#5243) Regression surfaced by #5009 / #5237 (the flex header ad). No approved issue — reporting and fixing in one go. ## Description: With the Playwire flex header ad on the page, opening any modal (Help, Store, Settings, …) rendered it **unstyled and unscrollable**. **Cause.** `o-modal` is shadow DOM and gets the page's Tailwind by copying the document's stylesheets into a constructed sheet (`SharedStyles.ts`). In dev, `Main.ts` imports the components ahead of `styles.css`, so the first copy runs before Vite has injected the `<style>` tags (~18 rules), and the module re-read on `window` **`load`**. The GumGum creative keeps an iframe loading indefinitely, so `load` never fires — measured `document.readyState === "interactive"` 65s+ after the unit rendered. The re-read never happened, so the modal had no `overflow-y-auto` / `flex-1` / `min-h-0`, grew to its full content height inside the clipped page (`[data-modal-scroll]` computed `overflow-y: visible`, `clientHeight == scrollHeight`), and could not scroll. **Fix.** Re-read on `DOMContentLoaded` instead. It fires right after the deferred module scripts finish executing (verified there is no top-level `await` in the client graph), and ad iframes cannot hold it up. Prod is unaffected either way: the `<link rel="stylesheet">` is already in `<head>` at module-evaluation time, so the initial copy works there and this path is dev-only insurance — but the `load` dependency was a footgun regardless. Not touched: `SharedStyles` also copies whatever `<style>`/`<link>` ad scripts have injected by the time it runs (`#pwCSSstyling`, the Playwire config stylesheet). That was already the case on `load`; with `DOMContentLoaded` it now runs *before* those exist, which is if anything cleaner. ## Verification Headed Chromium against `npm run dev` with the real creative (GAM preview URL + Playwire requesting the `flex` unit), 1400×900: - **Before:** Help open under the ad → shadow sheet has 18 rules, no `.overflow-y-auto`; scroll box `overflow-y: visible`, 6463px tall; wheel does nothing. - **After:** shadow sheet fully populated; scroll box `overflow-y: auto`, 259px tall with 6704px of content; wheel moves `scrollTop` 0 → 1500, document stays put. Store page scrolls too. - **No ad** (headless, desktop 1400×900 + mobile 390×844): unchanged — Help scrolls inside the modal, document locked, as after #5148. - Prettier + ESLint clean. ## Please complete the following: - [x] I have added screenshots for all UI updates — no visual change; the fix restores the modal's existing styling - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file — N/A - [x] I have added relevant tests to the test directory — N/A, depends on the live Playwire script / document load timing; verified e2e as above ## Please put your Discord username so you can be contacted if a bug or regression is found: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Code <noreply@anthropic.com>
Regression from the flex header ad (#5009 / #5237). No approved issue — reporting and fixing in one go. ## Description: With the flex header ad showing, opening any inline page modal (Store, Settings, Leaderboard, ...) leaves the modal only a sliver of height, so it effectively can't be scrolled. **Cause.** `body.page-open` locks the document at scroll 0 (#5148). Playwire's flex unit expands (350px desktop / 210px mobile) whenever its container intersects the viewport and only docks to the 100px/70px fixed leaderboard once the container scrolls out — which can never happen while the page is locked. So the expanded ad sat in flow above the modal for as long as the page was open. On an 800px-tall viewport the modal got ~220px. **Fix (CSS only, `index.html`).** While a page is open: - park `#pw-oop-flex_container` just above the viewport (`position: fixed; bottom: 100%`) so Playwire's IntersectionObserver docks the unit; - pin the docked unit to `top: 0` (Playwire docks with `top: auto`, i.e. the static position, which is now above the viewport); - pad the content area by `--top-ad-height` (already measured by HomepagePromos) and make the desktop nav static, since nothing scrolls in that state. When the page closes the container returns to flow and the unit re-expands as before. Verified in headed Chromium with the Playwire preview URL on desktop (1400×800) and mobile (390×844): the unit docks within Playwire's collapse delay, the modal scroller reaches its full extent, closing restores the expanded unit and document scrolling, and opening a page from a scrolled position keeps the docked unit in place. Before: the expanded ad sat in flow above the Settings modal, which was ~220px tall. After: the ad is docked at the top and the modal fills the area below the nav. ## Please complete the following: - [ ] I have added screenshots for all UI updates (layout verified in-browser; described above) - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file (no new text) - [x] I have added relevant tests to the test directory (CSS-only layout change; verified in a real browser against Playwire's live script) ## Please put your Discord username so you can be contacted if a bug or regression is found: evanpelle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Regression fix for #5009 (
7ae09256b). No approved issue — reporting and fixing in one go.Description:
Since #5009, opening an inline page modal (Store, Settings, Leaderboard, Help, ...) scrolls the whole document instead of scrolling inside the modal.
Cause. Those modals are the
.page-contentelements inindex.html, sizedw-full h-full. They used to get a bounded height from the locked chain #5009 removed:body h-full overflow-hidden→ content divh-full overflow-hidden→main-layout'soverflow-y-auto. Unbounding that chain is correct for the homepage (document-level scrolling, needed for ad viewability), but it also let the page modals grow to their full content height, soo-modal's[data-modal-scroll]container had nothing left to scroll. Measured atmainwith Help open: documentscrollHeight7353px, modal containerclientHeight == scrollHeight == 7034.Fix. Re-lock the layout only while an inline page is open, keyed off a
page-openbody class — the same pattern as the existingin-gameclass:src/client/Navigation.ts—showPage()togglesbody.page-open. It's the single place page visibility changes;BaseModal.open()/close()for inline modals route through it.src/client/styles.css—body.page-open { height: 100%; overflow: hidden; flex-direction: column; flex-wrap: nowrap }. The column matters: Make homepage scrollable and shift UI below the header ad #5009 made body a wrapping flex row so the Playwire header-ad slot (#pw-oop-flex_container) gets its own full-width line. A column lets the content area take the height remaining under that slot rather than overflowing by its height.index.html— content area getsin-[.page-open]:min-h-0 in-[.page-open]:overflow-hidden; the ad slot getsflex-basis: auto; flex-shrink: 0underbody.page-open, since its usualflex-basis: 100%would mean 100% height in a column.src/client/components/MainLayout.ts—min-h-0onmainand the inner column so they can shrink to the bounded height. This is what the removedoverflow-hidden/overflow-y-autoimplicitly gave them; inert on the homepage, whose chain is auto-height.The homepage keeps scrolling at the document level exactly as #5009 intends — the class is only present while a page modal is open.
Verification
Headless Chromium against the dev server, at 1400×900 and 390×844:
main: Help open → documentscrollHeight7353 / 10470,window.scrollTo(0, 5000)moves the page, modal container doesn't scroll.scrollHeight == innerHeight(900 / 844),window.scrollTodoesn't move, modal container scrolls (7034px of content in a 581px box; 10284 in 658 on mobile), footer pinned at the viewport bottom.#pw-oop-flexunit in its slot with a page open → slot occupies 0–250, nav sits at 250, the modal's scroll box shrinks to 331px and still scrolls, footer still at the viewport bottom, no document scroll.Not covered: the real Playwire creative (RAMP won't initialize headless). The simulation above reproduces the geometry #5009 describes, but a headed pass with ads live is worth a glance.
Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
evanpelle
🤖 Generated with Claude Code