diff --git a/.changeset/rotten-jars-shout.md b/.changeset/rotten-jars-shout.md new file mode 100644 index 000000000..735905c29 --- /dev/null +++ b/.changeset/rotten-jars-shout.md @@ -0,0 +1,15 @@ +--- +"@hyperbook/markdown": patch +"@hyperbook/cloud": patch +hyperbook: 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); + } +}