From ae6a3ab1b0eef44f423ae4293efd158ac5509b15 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 06:40:07 +0000 Subject: [PATCH 1/2] fix(markdown): let the document scroll on narrow screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reading shell pinned itself to the viewport — `.main-grid { position: fixed }` over `html, body { overflow: hidden }` — and scrolled `main` inside it, so the sidebar could stay put while the article moved. The document scroller then never moves, and a mobile browser drives both pull-to-refresh and the collapsing address bar off exactly that scroller, so neither gesture ever fires. Below the 1280px breakpoint none of that is being paid for: the sidebar is already a drawer and the grid is a single column. Unwind the shell back into normal flow there and hold the header in place with `position: sticky` instead. Sticky needs a block-level shell — a grid item sticks only within its own grid area, which for the header is the one row it already fills — and the rules have to come last in the file, since they override the base layout on equal specificity. Wider screens keep the fixed grid. Two consequences of the document becoming the scroller: anchor jumps get `scroll-padding-top` so a heading clears the sticky header, and the scroll listener in the store, which until now never fired on mobile, coalesces its writes to one per frame rather than one per scroll event. The cloud shell carries the same layout and gets the same treatment. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014nCXuDptSw9FUW4WagZjak --- .changeset/rotten-jars-shout.md | 14 ++++ packages/markdown/assets/print.css | 3 + packages/markdown/assets/shell.css | 73 ++++++++++++++++++++ packages/markdown/assets/store.js | 27 ++++++-- platforms/cloud/public/stylesheets/style.css | 40 +++++++++++ 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 .changeset/rotten-jars-shout.md diff --git a/.changeset/rotten-jars-shout.md b/.changeset/rotten-jars-shout.md new file mode 100644 index 000000000..33e2484c0 --- /dev/null +++ b/.changeset/rotten-jars-shout.md @@ -0,0 +1,14 @@ +--- +"@hyperbook/markdown": patch +"@hyperbook/cloud": patch +--- + +Restore pull-to-refresh and the collapsing address bar on mobile + +On narrow screens the shell no longer pins itself to the viewport. It scrolled +the article pane inside a fixed grid, which left the document scroller +motionless — and a mobile browser drives both pull-to-refresh and the +auto-hiding address bar off that scroller, so neither ever fired. Below 1280px +the layout now flows normally with a sticky header, so the document scrolls and +both gestures work again. The desktop layout, where the sidebar needs to stay +put while the article moves, is unchanged. diff --git a/packages/markdown/assets/print.css b/packages/markdown/assets/print.css index 00b3ebd9f..0b3338b03 100644 --- a/packages/markdown/assets/print.css +++ b/packages/markdown/assets/print.css @@ -7,6 +7,9 @@ * page printed without these rules stops after the first sheet — everything * below the fold is clipped rather than flowed onto the next page. * + * Narrow screens already unwind that layout (see the last block in shell.css), + * but they do it behind `@media screen`, so print never inherits it. + * * These rules unwind the layout back into normal document flow, drop the * interactive chrome, and open anything collapsed so the printed sheet carries * the same content the reader can see on screen. diff --git a/packages/markdown/assets/shell.css b/packages/markdown/assets/shell.css index 3e10b1055..b3339a7a4 100644 --- a/packages/markdown/assets/shell.css +++ b/packages/markdown/assets/shell.css @@ -1303,3 +1303,76 @@ img.emoji { display: inline-block; max-width: none; } + +/* + * Narrow screens: give scrolling back to the document. + * + * The reading layout pins the shell to the viewport (`.main-grid { position: + * fixed }`, plus `html, body { overflow: hidden }` from the inline root CSS) + * and scrolls `main` on its own, so the sidebar can stay put while the article + * moves. The document itself then never scrolls — and a mobile browser drives + * both pull-to-refresh and the collapsing address bar off the *document* + * scroller, so neither gesture ever fires. + * + * Below the breakpoint none of that is being paid for: the sidebar is already + * a drawer and the grid is a single column. So unwind the shell back into + * normal flow and hold the header in place with `position: sticky` instead. + * + * The rules must come after the base layout in source order — they override it + * on equal specificity — and the shell has to be block-level for the sticky + * header: a grid item sticks only within its own grid area, which for the + * header is exactly the row it already fills. + * + * These rules live after every other breakpoint in this file on purpose. Keep + * them last. + */ +@media screen and (max-width: 1280px) { + html, + body { + overflow: visible; + height: auto; + width: auto; + } + + html { + /* Jumping to a heading has to clear the sticky header. */ + scroll-padding-top: var(--header-height); + } + + body { + /* Rubber-banding past the end exposes the canvas, which without this + paints white behind a dark page. */ + background: var(--color-background); + /* The document is the scroller now, so its scrollbar is the one worth + showing — the article pane had one here before. */ + scrollbar-width: auto; + } + + body::-webkit-scrollbar { + display: block; + } + + .main-grid { + display: block; + position: static; + min-height: 100dvh; + } + + main { + overflow: visible; + } + + header { + position: sticky; + top: 0; + /* The grid row used to size the header; in flow it has to size itself. */ + height: var(--header-height); + box-sizing: border-box; + } + + /* The impersonation banner is fixed to the top of the viewport, so the header + sticks below it rather than sliding underneath. */ + #impersonation-banner + .main-grid header { + top: 36px; + } +} diff --git a/packages/markdown/assets/store.js b/packages/markdown/assets/store.js index 39e39a7c4..98e5f64fa 100644 --- a/packages/markdown/assets/store.js +++ b/packages/markdown/assets/store.js @@ -166,12 +166,27 @@ hyperbook.store = (function () { window.addEventListener("mousemove", (e) => { db.currentState.update(1, { mouseX: e.clientX, mouseY: e.clientY }); }); - window.addEventListener("scroll", (e) => { - db.currentState.update(1, { - scrollX: window.scrollX, - scrollY: window.scrollY, - }); - }); + // On narrow screens the document is the scroller, so this fires for the + // whole length of every fling. Coalescing to one write per frame keeps a + // scroll gesture from queueing an IndexedDB write per scroll event. + let scrollWritePending = false; + window.addEventListener( + "scroll", + () => { + if (scrollWritePending) { + return; + } + scrollWritePending = true; + requestAnimationFrame(() => { + scrollWritePending = false; + db.currentState.update(1, { + scrollX: window.scrollX, + scrollY: window.scrollY, + }); + }); + }, + { passive: true }, + ); window.addEventListener("resize", (e) => { db.currentState.update(1, { windowWidth: window.innerWidth, diff --git a/platforms/cloud/public/stylesheets/style.css b/platforms/cloud/public/stylesheets/style.css index 80b7a73a1..01e55afd4 100644 --- a/platforms/cloud/public/stylesheets/style.css +++ b/platforms/cloud/public/stylesheets/style.css @@ -344,3 +344,43 @@ nav.breadcrumb { .data-table th, .data-table td { padding: 8px 6px; font-size: 0.85rem; } .actions { flex-wrap: wrap; } } + +/* + * Narrow screens: give scrolling back to the document. + * + * The shell above pins itself to the viewport and scrolls `main` on its own, + * which leaves the document scroller motionless — and a mobile browser drives + * pull-to-refresh and the collapsing address bar off that scroller, so neither + * gesture ever fires. Unwind the grid into normal flow and keep the header in + * place with `position: sticky` instead. Mirrors the reading shell in + * packages/markdown/assets/shell.css. + * + * Must stay after the base layout in source order: these override it on equal + * specificity. A grid item can only stick inside its own grid area, hence the + * block-level shell. + */ +@media screen and (max-width: 1280px) { + html, body { + overflow: visible; + height: auto; + width: auto; + } + + html { scroll-padding-top: var(--header-height); } + + body { background: var(--color-background); } + + .main-grid { + display: block; + position: static; + min-height: 100dvh; + } + + main { overflow: visible; } + + header { + position: sticky; + top: 0; + height: var(--header-height); + } +} From ea0a1725e019cad8751a2aadca489e6d5aa7d3b2 Mon Sep 17 00:00:00 2001 From: Mike Barkmin Date: Fri, 14 Aug 2026 09:17:40 +0200 Subject: [PATCH 2/2] Aktualisieren von rotten-jars-shout.md --- .changeset/rotten-jars-shout.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/rotten-jars-shout.md b/.changeset/rotten-jars-shout.md index 33e2484c0..735905c29 100644 --- a/.changeset/rotten-jars-shout.md +++ b/.changeset/rotten-jars-shout.md @@ -1,6 +1,7 @@ --- "@hyperbook/markdown": patch "@hyperbook/cloud": patch +hyperbook: patch --- Restore pull-to-refresh and the collapsing address bar on mobile