Skip to content

feat(webapp): read a file with the whole browser window (BEA-195) - #215

Merged
ssowonny merged 2 commits into
mainfrom
bea-195-fullscreen
Sep 11, 2026
Merged

ssowonny merged 2 commits into
mainfrom
bea-195-fullscreen

Conversation

@ssowonny

Copy link
Copy Markdown
Contributor

TL;DR

  • Reading a wide CSV, a big diagram, a rendered HTML file or a PDF through the app's letterbox is over: there's a fullscreen control on every file page.
  • It's a URL: ?full=1. Copy it, send it, and a teammate lands fullscreen on the same file — which is what the issue actually asked for.
  • Exit button, Esc, or browser Back all leave. The button is always on screen because Esc genuinely cannot reach us from inside the HTML sandbox or the PDF viewer.
  • Your scroll position survives both directions — the trap here is that adding a query param looks like a brand-new route to the scroll machine.
  • Most of this was written earlier and never opened as a PR; I rebased it onto main, resolved the conflicts, and found one real bug it couldn't have known about (below).

Before / after

A 10-column CSV at 1440px. Today it's clipped at the reading column; with ?full=1 every column fits.

Today ?full=1

Markdown deliberately keeps its 768px measure — a 2000px line of prose is unreadable, so what prose gains here is the removed chrome, not a wider column:

Today ?full=1

Why a query param

The first path segment after the project id is reserved for view names, and a file path already occupies that slot. ?v=<sha> is the existing precedent and the reasoning is identical: a fullscreen file is the same page with different chrome, exactly as a pinned version is the same page with older bytes. The two compose — ?v=<sha>&full=1 reads a past version fullscreen, banner and all.

The two traps, both real

1. Entering fullscreen threw the reader to the top of the document. routeKey is useLocationPath() — pathname plus search — so a ?full=1 push looked like a fresh route and armed a scroll goal of 0. You ask for more of a document and lose your place in it. Fixed by keying the scroll memo on withoutFull(...), so entering and leaving land in the same slot.

2. Esc can never reach the app from inside an HTML or PDF file. Those render in iframes — HTML under sandbox="allow-scripts" (an opaque origin), PDF as the browser's own viewer. Key events don't cross that boundary. So the Exit control is rendered by AppShell outside the topbar it hides, painted over the content, always visible and never a hover reveal. It is the only exit that works from inside one.

The chrome is hidden by a body class, never unmounted: <article id="content"> survives the toggle and keeps its scrollTop for free, and display: none takes the hidden controls out of the tab order and the accessibility tree together — the same lesson syncSidebarInert already encodes for the off-canvas sidebar.

The bug the earlier work could not have known about

Fullscreen and the print view (BEA-219, merged a few hours ago) overlap: #exit-full is position: fixed over the content, so printing a file opened fullscreen stamped the Exit button onto the page, above a blank strip reserved for it. It's now in the print block's hidden-chrome list with its padding dropped, and there's a regression test — verified to fail when the rule is removed.

Testing

  • e2e/fullscreen.spec.ts — 11 specs, one per acceptance criterion: chrome hidden, pasted link with no flash, ?v= composition, Esc/Back/Exit, scroll preserved both directions, no refetch on toggle, Exit over the HTML and PDF iframes, tab order and focus, markdown measure vs HTML frame height, below the 900px breakpoint, and folders/view routes ignoring ?full=1.
  • router.test.tsfull parsed, round-tripped, surviving alongside ?v=, and withoutFull idempotent. 138 unit tests pass.
  • Full e2e: 236 passed. The 7 admin/org failures reproduce identically on clean main (an org-rename test cascading) — I baselined that with a full run earlier today.
  • go test ./... green. check-dist.sh reports the committed bundle fresh.

Architecture changes

Browser gains the fullscreen capability and router gains Route.full, withoutFull, and a fourth urlForPath parameter. architecture/webapp-frontend.md is updated on the branch; excerpt of the change:

✅ added · ❌ removed (strikethrough) · unmarked = unchanged

flowchart TB
    Browser["<div style='text-align:left'><b>Browser</b><br/>folder listing, file view<br/>per-view routes<br/>+moved: /resolve?path= on a tree miss only<br/>+scroll restoration: contentRef, memo, goal from lib/scroll<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +fullscreen: body.full-view from route.full, Exit / Esc / Back</span></div>"]
    router["<div style='text-align:left'><b>router</b><br/>+parseRoute(url, mode) Route<br/>+Route.version ?v= sha, one past version<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +Route.full ?full=1, the file page with the chrome hidden</span><br/>+Route.trailingSlash / Route.queryTarget / Route.filters<br/>+historyFilterQuery(filters) / hasHistoryFilters<br/><span style='background:#ef444455;padding:0 4px;border-radius:3px'>❌ <s>+urlForPath(path, projectId, version)</s></span><br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +urlForPath(path, projectId, version, full)</span><br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +withoutFull(url) the same URL minus full — the scroll memo key</span><br/>+urlForView(...) / encodePath / decodePath<br/>+projectByName(projects, seg)</div>"]
    Why["✅ withoutFull exists for the scroll memo.<br/>routeKey is pathname+search, so a ?full=1 push<br/>looked like a fresh route and armed a goal of 0 —<br/>the reader was thrown to the top of the document<br/>the moment they asked for more of it."]
    Browser --> router
    Browser -.- Why
    classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px
    classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2
    class Why added
Loading

Closes BEA-195.

🤖 Generated with Claude Code

ssowonny and others added 2 commits September 10, 2026 17:49
A file page renders inside the app shell — a 264px sidebar, a topbar, and a
768/1200px column — so a wide CSV, a big mermaid diagram, a rendered HTML
file or a PDF is read through a letterbox. `?full=1` on the file route hides
the chrome and gives the content the window; Exit, Esc and browser Back all
leave, and the URL is the state, so a teammate you paste it to lands
fullscreen on the same file.

Fullscreen rides as a query param beside `?v=` rather than as a view route,
for the same reason a version does: the first segment after the project id is
reserved for view names, and a fullscreen file is the same page with
different chrome. The two compose — `?v=<sha>&full=1` reads a past version
fullscreen, banner and all.

Two things the code forced:

  * `routeKey` is `withoutFull(useLocationPath())`. That key is
    pathname+search, so a `?full=1` push otherwise looks like a brand-new
    route and arms a scroll goal of 0 — the reader is thrown to the top of
    the document the moment they ask for more of it.
  * The Exit control is permanently visible and painted over the content,
    not a hover reveal. The HTML renderer is a sandboxed iframe on an opaque
    origin and the PDF renderer is the browser's own viewer; once focus is
    inside either, `keydown` never reaches us and Esc silently does nothing.

The chrome is hidden with a body class, never unmounted. `<article
id="content">` therefore survives the toggle and keeps its `scrollTop` in
both directions for free, and `display: none` takes the hidden controls out
of the tab order and the accessibility tree together — the same lesson
`syncSidebarInert` already encodes for the off-canvas sidebar.

Markdown keeps the `--page-read` measure: a 2000px line of prose is
unreadable, and what prose gains here is the removed chrome, not a wider
column. Every other renderer gets `.page.bleed` and the HTML/PDF frames take
the viewport height.

Frontend only — no new API, no config, no telemetry, and `full` never
reaches a query key, so a toggle refetches nothing and inflates no read
count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fullscreen (BEA-195) and the print view (BEA-219) landed hours apart and
overlap: #exit-full is fixed over the content, so printing a file opened
fullscreen stamped the Exit button onto the page, above a blank strip
reserved for it. Added to the print block's hidden chrome, with the
reserved padding dropped alongside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ssowonny
ssowonny merged commit 9b3eb19 into main Sep 11, 2026
2 checks passed
@ssowonny
ssowonny deleted the bea-195-fullscreen branch September 11, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant