Preview panel: resizable right-side file preview (images, PDF, video/audio, MD, text) #295

Closed
opened 2026-08-20 20:40:12 +00:00 by Cordy · 4 comments
Owner

Request (2026-08-20, Nikola): clicking a file row should open a preview panel sliding in from the right (like Dropbox but not half the page) — default ~1/3 width, draggable border to resize. Mockup approved 2026-08-20; phasing decision: native formats first, office formats follow in #TBD (phase 2).

Scope v1 — zero-dependency formats:

  • Images (png/jpg/jpeg/gif/webp/svg/bmp/avif): native <img>.
  • PDF: browser-native viewer in <iframe>. Requires an inline mode on the raw endpoint — it currently always sends Content-Disposition: attachment, which is exactly why nothing renders in-browser today. Add ?inline=1Content-Disposition: inline + correct Content-Type from the extension.
  • Video/audio (mp4/webm/mp3/ogg/wav/m4a): native <video>/<audio> (also needs inline + Range support — http.ServeContent already does ranges).
  • Markdown: small built-in renderer, escape-first (a shared .md must not be able to inject script) — headings, lists, code blocks, links, bold/italic, tables.
  • Text/code (txt/json/yaml/csv/go/js/css/html-as-text/log/…): escaped <pre>, capped (~2 MB; larger shows "too large — download").
  • Everything else: file card with type/size + Download button (no dead panel).

Panel UX (per approved mockup):

  • Click anywhere on a row (except action buttons) opens the panel; clicking another row swaps content in place; Esc or ✕ closes; same row click toggles.
  • Default width 1/3 of the window; drag the divider (min ~280px, max ~70%); width persisted in localStorage.
  • Header: filename (ellipsized), type · size · modified, favorite star (#296), close. Footer: Download, Open (raw in new tab).
  • Row click no longer immediately downloads — Download moves to the panel/action buttons (this is the main behavior change; flag in CHANGELOG).
  • Preview fetches through the scoped raw endpoint → counts as an open and lands in Recents (consistent with "views count" semantics).

Security note: ?inline=1 means user-uploaded HTML/SVG could execute in the app origin if iframed naively. Mitigation: serve inline previews for a whitelist of safe content types only (images/pdf/video/audio); SVG previews via <img> (no script execution), never iframe; HTML files preview as escaped text in v1. Add X-Content-Type-Options: nosniff on raw responses.

Mockup-first done; fidelity diff against the approved mockup before ship per house process.

**Request (2026-08-20, Nikola):** clicking a file row should open a preview panel sliding in from the right (like Dropbox but not half the page) — default ~1/3 width, draggable border to resize. Mockup approved 2026-08-20; phasing decision: native formats first, office formats follow in #TBD (phase 2). **Scope v1 — zero-dependency formats:** - **Images** (png/jpg/jpeg/gif/webp/svg/bmp/avif): native `<img>`. - **PDF:** browser-native viewer in `<iframe>`. Requires an inline mode on the raw endpoint — it currently always sends `Content-Disposition: attachment`, which is exactly why nothing renders in-browser today. Add `?inline=1` → `Content-Disposition: inline` + correct Content-Type from the extension. - **Video/audio** (mp4/webm/mp3/ogg/wav/m4a): native `<video>`/`<audio>` (also needs inline + Range support — `http.ServeContent` already does ranges). - **Markdown:** small built-in renderer, escape-first (a shared .md must not be able to inject script) — headings, lists, code blocks, links, bold/italic, tables. - **Text/code** (txt/json/yaml/csv/go/js/css/html-as-text/log/…): escaped `<pre>`, capped (~2 MB; larger shows "too large — download"). - **Everything else:** file card with type/size + Download button (no dead panel). **Panel UX (per approved mockup):** - Click anywhere on a row (except action buttons) opens the panel; clicking another row swaps content in place; Esc or ✕ closes; same row click toggles. - Default width 1/3 of the window; drag the divider (min ~280px, max ~70%); width persisted in localStorage. - Header: filename (ellipsized), type · size · modified, favorite star (#296), close. Footer: Download, Open (raw in new tab). - Row click no longer immediately downloads — Download moves to the panel/action buttons (this is the main behavior change; flag in CHANGELOG). - Preview fetches through the scoped raw endpoint → counts as an open and lands in Recents (consistent with "views count" semantics). **Security note:** `?inline=1` means user-uploaded HTML/SVG could execute in the app origin if iframed naively. Mitigation: serve inline previews for a **whitelist** of safe content types only (images/pdf/video/audio); SVG previews via `<img>` (no script execution), never iframe; HTML files preview as escaped text in v1. Add `X-Content-Type-Options: nosniff` on raw responses. Mockup-first done; fidelity diff against the approved mockup before ship per house process.
Author
Owner

Shipped as v0.6.57 (PR #298), live on both dogfoods. Fidelity diff against the approved mockup was done pre-merge and three gaps were fixed on the branch: always-teal divider grip, teal outline on the selected row, and the amber star shown next to the filename (not just in the actions column).

What changed:

  • Clicking a file row or its name opens the preview panel on the right — no more instant download. Download lives in the panel footer, the row's ⬇ icon, and the row menu. Clicking the same row again (or Escape / ✕) closes it; clicking another file swaps the preview in place.
  • Default width ⅓ of the window; drag the divider to resize (280px up to 70%); your width is remembered. On phones the panel goes full-screen instead.
  • Previews: images, PDF (browser viewer), video/audio, Markdown (rendered), text/code (up to 2 MB). Anything else (incl. office files until #297) shows a card with Download.
  • Previewing counts as opening → shows up in Recents.
  • Security: inline serving is whitelist-only; HTML always downloads (never renders in the app origin); SVG previews sandboxed; nosniff on all raw responses.

Dogfood checklist:

  • Click an image row → preview appears; clicking the filename does the same (no download)
  • PDF renders in the panel via the browser viewer
  • A video plays; an mp3 shows an audio player
  • A .md renders formatted; a .txt/.json/.go shows as text
  • A .docx shows the "no preview" card with working Download (office = #297)
  • Drag the divider — width changes, persists across reload; can't go absurdly narrow/wide
  • Escape and ✕ close; clicking another row swaps content; same row toggles
  • Selected row is highlighted (teal top/bottom edge)
  • Previewed files appear in Recents
  • Row ⬇ icon and row-menu Download still download directly
  • An uploaded .html file downloads, never renders
Shipped as **v0.6.57** (PR #298), live on both dogfoods. Fidelity diff against the approved mockup was done pre-merge and three gaps were fixed on the branch: always-teal divider grip, teal outline on the selected row, and the amber star shown next to the filename (not just in the actions column). **What changed:** - Clicking a file row **or its name** opens the preview panel on the right — no more instant download. Download lives in the panel footer, the row's ⬇ icon, and the row menu. Clicking the same row again (or Escape / ✕) closes it; clicking another file swaps the preview in place. - Default width ⅓ of the window; drag the divider to resize (280px up to 70%); your width is remembered. On phones the panel goes full-screen instead. - Previews: images, PDF (browser viewer), video/audio, Markdown (rendered), text/code (up to 2 MB). Anything else (incl. office files until #297) shows a card with Download. - Previewing counts as opening → shows up in Recents. - Security: inline serving is whitelist-only; HTML always downloads (never renders in the app origin); SVG previews sandboxed; `nosniff` on all raw responses. **Dogfood checklist:** - [ ] Click an image row → preview appears; clicking the filename does the same (no download) - [ ] PDF renders in the panel via the browser viewer - [ ] A video plays; an mp3 shows an audio player - [ ] A .md renders formatted; a .txt/.json/.go shows as text - [ ] A .docx shows the "no preview" card with working Download (office = #297) - [ ] Drag the divider — width changes, persists across reload; can't go absurdly narrow/wide - [ ] Escape and ✕ close; clicking another row swaps content; same row toggles - [ ] Selected row is highlighted (teal top/bottom edge) - [ ] Previewed files appear in Recents - [ ] Row ⬇ icon and row-menu Download still download directly - [ ] An uploaded .html file downloads, never renders
Author
Owner

Hotfixes shipped as v0.6.58 (PR #299), live on both dogfoods.

Root causes of the three findings:

  1. Crushed header: the panel was built as an <aside> element — Cairn's sidebar has a global aside button { width: 100% } rule, so the star and ✕ each demanded the full panel width and squeezed the filename to nothing. The panel is now a <section>, and .pvicon is pinned to its natural size as a second guard.
  2. Broken row highlight: the top/bottom inset shadow lines collided with the table's borders at the left/right edges (disappearing ends + the 1px shift). Replaced with a seamless 3px teal accent bar on the left of the row plus the background tint.
  3. Page stretching on long files: the panel now sticks to the viewport at 100vh − header and its body scrolls internally — a long .md scrolls inside the preview, PDFs stay contained in their viewer (first page visible, scroll within the frame). #297 office previews will use the same contained model (render capped/first page in the fixed-height body).

Re-check:

  • Open the same .md — filename, star and ✕ sit on one tidy header line
  • Long .md scrolls inside the panel; the page itself no longer grows
  • PDF contained the same way
  • Selected row: clean left teal bar + tint, no broken edges or shifted border
Hotfixes shipped as **v0.6.58** (PR #299), live on both dogfoods. **Root causes of the three findings:** 1. **Crushed header:** the panel was built as an `<aside>` element — Cairn's sidebar has a global `aside button { width: 100% }` rule, so the star and ✕ each demanded the full panel width and squeezed the filename to nothing. The panel is now a `<section>`, and `.pvicon` is pinned to its natural size as a second guard. 2. **Broken row highlight:** the top/bottom inset shadow lines collided with the table's borders at the left/right edges (disappearing ends + the 1px shift). Replaced with a seamless 3px teal accent bar on the left of the row plus the background tint. 3. **Page stretching on long files:** the panel now sticks to the viewport at `100vh − header` and its body scrolls internally — a long .md scrolls inside the preview, PDFs stay contained in their viewer (first page visible, scroll within the frame). #297 office previews will use the same contained model (render capped/first page in the fixed-height body). **Re-check:** - [ ] Open the same .md — filename, star and ✕ sit on one tidy header line - [ ] Long .md scrolls inside the panel; the page itself no longer grows - [ ] PDF contained the same way - [ ] Selected row: clean left teal bar + tint, no broken edges or shifted border
Author
Owner

Row-separator seams fixed in v0.6.59 (PR #300), live on both dogfoods.

Honest root-cause note: the v0.6.58 highlight fix was correct for the selected-row effect, but the seams you circled were a different, pre-existing bug that the longer wrapped filenames made visible: td.namecell was itself display: flex. A table cell with display: flex stops being a table cell — the browser wraps it in an anonymous cell and its borders no longer collapse with the size/modified cells, producing the 1px steps at the column boundary and the misaligned dash on folder rows. The cell is a real table cell again; the icon+name flex layout now lives on a wrapper div inside it. Verified there is exactly one place in the UI that builds these cells.

Re-check: the line under every row should now run continuously across all four columns — including the two-line wrapped names and the Inbox/moverini folder rows from your screenshot.

Row-separator seams fixed in **v0.6.59** (PR #300), live on both dogfoods. **Honest root-cause note:** the v0.6.58 highlight fix was correct for the *selected-row* effect, but the seams you circled were a different, pre-existing bug that the longer wrapped filenames made visible: `td.namecell` was itself `display: flex`. A table cell with `display: flex` stops being a table cell — the browser wraps it in an anonymous cell and its borders no longer collapse with the size/modified cells, producing the 1px steps at the column boundary and the misaligned `—` dash on folder rows. The cell is a real table cell again; the icon+name flex layout now lives on a wrapper `div` inside it. Verified there is exactly one place in the UI that builds these cells. **Re-check:** the line under every row should now run continuously across all four columns — including the two-line wrapped names and the Inbox/moverini folder rows from your screenshot.
Author
Owner

Closing — verified on the dogfood (shipped v0.6.57/58/59, PRs #298/#299/#300).

Closing — verified on the dogfood (shipped v0.6.57/58/59, PRs #298/#299/#300).
Cordy closed this issue 2026-08-26 17:40:10 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#295
No description provided.