Keep the reader in place when the drawer closes, and stop the CSS promising a header it deleted
CI / full-gate (push) Successful in 2m54s

This commit is contained in:
2026-09-09 15:52:46 +02:00
parent f959404c6f
commit 8128ec2f42
5 changed files with 44 additions and 20 deletions
+13 -14
View File
@@ -36,12 +36,15 @@ branch, create a PR and merge it when the CI/CD turns green.
## Project priorities (do not erode)
1. **Simplicity** — prefer the solution that is easiest to understand, smallest, and most readable.
**A page is a document**: it scrolls, and the chrome scrolls with it. Bounding the viewport to
hold something still — a `100dvh` box, `position: sticky`/`fixed` chrome, `overflow: hidden` on
`body` buys an app-like look with CSS the next reader has to reverse-engineer, and every such
box is one more thing to undo before the content under it can be reached. Sticky headers and
full-height panes do not earn that. The mobile off-canvas nav is the one exception, because an
overlay has no other spelling.
**A page is a document**: it scrolls, and the chrome scrolls with it. Nothing may bound the
viewport to hold content still — no `height: 100dvh` frame, no `overflow: hidden` on `body`, no
`position: sticky` header. Each such box buys an app-like look with CSS the next reader has to
reverse-engineer, and is one more thing to undo before the content under it can be reached.
Overlays are not this: the skip link, the mobile off-canvas nav and its scrim sit *above* the
document rather than holding it still, and have no other spelling — the document keeps scrolling
behind the open nav, accepted rather than overlooked. Only the document scroller gets keyboard
paging unconditionally and back/forward scroll restoration, and a page a box clips fails silently:
nothing in a test or a console says content is unreachable below the fold.
2. **Few dependencies** — runtime deps stay minimal (today `ejs`, `lucide-static`, `@larvit/log`,
`postgres`). Prefer the Node standard library; justify any new dependency; do not add frameworks.
The **host is stateless — it owns no schema and stores nothing of its own**; a plugin may own a
@@ -284,14 +287,10 @@ Revisit only if the stated reason stops holding.
it means disclosure rather than popup: the nav tree. `shell.ejs` hand-rolls the same block for the
profile menu (its trigger composes escaped user values and its one item is a CSRF POST form) — keep
the two in step.
- **Nothing bounds the viewport, so there is one scroller: the document.** `.app` is
`min-height: 100dvh`, never a `100dvh` box with `overflow: hidden`. It was the latter once, and a
page taller than the window lost everything past it — silently, in every engine, because nothing in
a test or a console says content is unreachable below the fold. Only the document scroller gets
**keyboard paging** unconditionally and **scroll restoration** on back/forward; a bounded region
gets keys only once focus is inside it, which without script needs a focusable descendant.
A plugin that wants a full-height pane owns that in its own stylesheet — the shell offers no opt-out
and no `.table thead` stickiness, per priority 1.
- **One scroller, the document** (priority 1). `.app` is `min-height: 100dvh`. `.nav`'s
`overflow-y: auto` and `.side-footer`'s `flex: 0 0 auto` are not leftovers of a bounded frame:
they are what makes the off-canvas panel usable with a long tree. `#nav-toggle` is `position: fixed`
for the same reason — a label click focuses it, and a browser scrolls a focused element into view.
- **`ICON_NAMES` (`src/ui/icons.ts`) is a host-owned registry, not a frozen plugin contract**, so it
is deliberately not re-exported from `@plainpages/plugin-api`. The palette may narrow when the last reference
to an id goes, and a plugin needing one gets it re-registered in the same change. Accepted cost: an