Reader mode — the focus overlay (plan)
Status: draft — recorded 2026-06-14 (Fritz). Next-session candidate. Fritz: "I really like the layout, it keeps the context nicely. One thing that would help: a quick reader mode that overlays in the middle the document you want to focus on" — since the trail has multiple columns, sometimes you want to sink into one document.
One-line
A reader overlay: the focused document rendered centered, at a comfortable measure, over the dimmed trail — context preserved behind it, one click (or Back) to return. A lens over the trail, NOT a new pane: the reasoning chain is untouched; you're just reading one link of it up close.
Why it fits the room
The trail's whole thesis is "context is spatial, never amputated." Reader mode
honors that: the columns stay on screen, dimmed — you can see where you are,
you just zoom your attention. It's the opposite of a modal that hides
everything; the context is still there, deliberately. Complements focus
(which pane gets the margin + live read) with a deeper "solo read" lens.
THE design decision: reader is URL state, not client state (ADR 0003)
Reader mode is a query param on the trail URL — /t/<trail>?reader=<paneIndex>
(0-based; omitted ⇒ no overlay; out of range ⇒ ignored). Consequences, all the
htmx-hard wins the trail already banks:
- Shareable: a link can say "here's the chain, start by reading pane 2."
- Browser Back closes it — temporal history over spatial state, free, no JS.
- Zero client state — the whole overlay renders server-side from the URL.
- Coexists with
?focus=(reader implies attention on that pane; keep both or fold focus into reader — settle when building).
Render
- The full canvas renders as today, with a dimmed backdrop layer over it.
- A centered reader panel renders the pane's already-fetched Document at the Tufte reading measure (≈60ch body + margin), so it's the same prose the column shows, just unconstrained by column width and centered.
- No extra world read: reader shows the Document already fetched for that pane (focused-live or cached) — the overlay is pure presentation.
- Margin stays (status, tags, provenance, backlinks, graph/edit/new/append affordances): reading mode is not a dead-end — you can still branch and act. Possibly collapsible; default shown (the trust layer is the point).
- SSR + CSS only; ADR 0003's canvas island stays unspent. Degrades: no CSS ⇒ the doc just renders after the canvas; no JS ⇒ close via the link / Back.
Affordance + close
- Each pane (header/spine) gets a "reader" control → link to
trailURL?reader=<i>. - Close: an explicit ✕ AND the backdrop itself are
<a>links back to the bare trail URL (no reader param). Optional Esc-to-close via a one-line _hyperscript snippet (an ADR 0003-sanctioned tiny concession) — or rely on Back. Recommend the snippet; it's the expected reader-mode gesture.
Load-bearing UX decision: does reader persist across link clicks?
- RECOMMENDED — persist: a link clicked inside the reader continues the
trail AND stays in reader on the newly-focused pane (read straight through the
argument without surfacing). Implemented by the reader panel's links using
trailAfterClickthen settingreader=<new focus index>. This is the feature's whole point ("focus on the document you want," then the next). - Alternative (simpler v1): links exit reader to the canvas, focused on the new pane. Less magical; falls out for free. Decide with Fritz; recommend persist.
Architecture (web-only — no port/service/domain change)
trail.go: parse thereaderquery param into thetrailstruct (e.g.Reader int, -1 = none);trailURLoptionally encodes it; a helper builds the reader/close links. The codec stays the single source of truth.trail_handlers.go: whenReaderis set and in range, render the canvas plus the reader-overlay view model (reuse the focused pane's Document; for persist-on-navigate, rewrite the reader panel's links to carry the new reader index).- Templates: a
readeroverlay block (backdrop + centered panel reusing the doc-body + doc-meta partials). CSS: backdrop, centered panel, dim. - Restrict to prose panes (doc/listing/tag); a floor/graph pane in reader is just a bigger SVG — allow later if wanted, not v1.
Tests
Codec round-trip with ?reader=; overlay renders the addressed pane; ✕/backdrop
link back to the bare trail; persist-on-navigate link rewriting; out-of-range
ignored; no-reader URL renders exactly today's canvas (no regression);
auth-gated affordances still present in the reader margin.
Scope / non-goals
- A presentation lens, not a pane kind — the trail path never changes.
- No client zoom/pan, no JS reader engine (htmx + CSS + at most a 1-line Esc hyperscript).
- Doc/listing/tag panes only for v1.
Open before building (settle with Fritz)
- Persist-on-navigate vs exit-on-navigate (recommend persist).
?reader=as its own param vs foldingfocusinto it.- Esc-to-close hyperscript vs Back-only.
- Margin shown vs collapsible in reader.
This is a self-contained next-session feature: web-layer only, no new reads, no broker/deploy coupling. Slots into the Reading Room family as "R4 — reader mode."
Decisions SETTLED + BUILT (2026-06-15, branch feature/reader-mode)
Fritz settled all four open questions — each matched the plan's recommendation:
- Persist-on-navigate — a link inside the overlay continues the trail AND stays in reader on the newly focused pane.
- Standalone
?reader=— its own query param, orthogonal to?focus=. - ✕ + backdrop + Esc — all three close; Esc via a 4-line vanilla handler
in islands.js (NOT
_hyperscript— pulling a ~40KB lib for one keybinding is worse than 4 lines in the ADR-0005-sanctioned canvas JS file). - Margin shown by default in the overlay (the trust layer is the point).
Load-bearing implementation decision: reader implies focus
A valid ?reader=<i> also takes Focus := i in parseTrail. This keeps the
single-live-read invariant (ADR 0005 d9): the overlay reuses the focused
pane's already-fetched Document — no second world read — and so always has
the full margin. Honors "standalone" (reader is its own param, present/absent
independently of focus) and the shareable "read pane 2" use case (?reader=2
focuses + overlays pane 2). Out-of-range / non-prose (floor, graph) / junk ⇒
no overlay (-1), never a 400 — a stale shared link degrades to the canvas.
Web-only build (no port/service/domain/broker/deploy change)
trail.go:trail.Reader int(-1 = none);parseTrailgains a 3rd arg (readerParam); newtrailReaderURL(t, reader)— the ONLY builder that emits?reader=;trailURLstays reader-free (canonical canvas URL, so every existing click closes the overlay).trailAfterClick/trailFocusedsetReader: -1.trail_links.go:trailizeLinks/trailizeNodegainreader bool— prose targets (doc/tag) persist the overlay, non-prose (graph/floor) exit it.trail_handlers.go:paneViewgainsreader bool(mode "reader"; skips RecordLinks on the overlay copy; backlinks persist, graph/map exit; no self-referential reader affordance in the overlay). Trail handler captures the focused doc and buildsvm.Reader *paneVM+vm.CloseURL(bare trail).ReaderURLadded topaneVMAND the single-doc page VM (unused there — the shareddoc-metatemplate references it).- Templates:
readeroverlay block (backdrop scrim + centered panel reusing doc-body + doc-meta) in canvas.html; "reader" affordance in the pane-head and the margin escape line (content.html). - islands.js: Esc-to-close (clicks the close link; hx-boost keeps it a swap).
- page.html: reader CSS (fixed backdrop, scrim-link behind panel, centered Tufte-width panel, dark-mode aware).
Tests: codec (reader takes focus, reader>focus precedence, tag overlays,
out-of-range/negative/junk/absent → -1, floor+graph rejected, trailReaderURL
round-trip + close URL); handler (overlay renders + canvas behind + margin ×2,
no-reader regression, out-of-range ignored, persist-on-navigate link rewrite,
auth affordances present + no self-reader-link in overlay). Gate green
(./pre-commit.sh, lint 0 issues). Live QUIC smoke passed: overlay renders
only on valid prose ?reader=, body links persist ?reader=, scrim/✕ → bare
trail, floor + out-of-range ignored.
Gotcha: a strings.Contains(body, "reader-panel") negative assertion
false-matched the .reader-panel CSS rule (the canvas page inlines all CSS).
Fix: assert the rendered element class="reader-backdrop", not the class name.
NOT committed (Fritz commits). Next: Fritz + CodeRabbit (@coderabbitai review) → merge (library 0.1.15) → deploy bump (reader-only, no broker/worlds/
agent change).