diff --git a/docs/plans/document-viewer-redesign-plan.md b/docs/plans/document-viewer-redesign-plan.md new file mode 100644 index 0000000000..de48df61fc --- /dev/null +++ b/docs/plans/document-viewer-redesign-plan.md @@ -0,0 +1,314 @@ +# Document viewer redesign — PDF + photo surfaces + +**Status:** plan only (no product behaviour change in this doc PR) +**Branch seed:** `cursor/document-viewer-redesign-plan-1db8` +**Flightplan evidence:** `.local/workflow-evidence/2026-08-06T17-27-05-553Z-flightplan.json` +**Related ledger:** `#214` (passive wheel / INP), `#219` (preview error announce), `#215` (image decode/priority) + +This is the execution plan for a dramatic improvement of design, style, approach, +functionality, and optimisation of the PDF reader and photo/figure viewers. It is +scoped to keep clinical source fidelity, phone search-chrome ownership, and signed-URL +privacy intact. + +--- + +## 1. Current state (what we are redesigning) + +### Surfaces + +| Surface | Today | Gap | +| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| PDF canvas (`pdf-canvas-viewer.tsx`) | pdf.js single-page raster, fit-width / zoom / rotate / fullscreen, gesture hook | Toolbar lives outside `DocumentFrame`; no page-linked figure sync; wheel always non-passive (#214); canvas errors not announced | +| PDF native iframe | “Sharper zoom” localStorage toggle | Uneven parity with canvas (no in-app gesture chrome); dual-engine cognitive load | +| Non-PDF whole-document image (`non-pdf-source-preview.tsx`) | Contained `` + open/download | No shared lightbox / gesture stack; weaker visual system than crops | +| Tables & diagrams rail (`source-panels` → `SignedImage` → `ImageLightbox`) | Vertical accordion list, deferred signed fetch, Sheet lightbox with CSS zoom/pan | No filmstrip / page sync; long lists not virtualized; lightbox missing `decoding=async` (#215) | +| `DocumentFrame` (COMPONENTS §6) | Shell-only in product (loading/error/surround) | Spec wants unified zoom/page metadata/viewing-aid ownership; PDF chrome still bespoke | + +### Architecture facts that drive the plan + +- `DocumentViewer.tsx` is still ~1.7k LOC after partial X3 extraction — load machine, + signed-URL refresh, summarize, phone chrome, and preview mode share one file. +- `document-viewer-lazy.tsx` is a re-export; real code-splitting is `next/dynamic` of the + PDF readers inside the shell (guarded by client-performance-boundary tests). +- Gestures are shared (`use-viewer-gestures.ts`) but applied differently: PDF pans via + scroll + re-raster; lightbox pans via CSS transform. +- Document detail routes own the floating search composer (`docs/search-chrome-behaviour.md`). + +--- + +## 2. Product north star + +One **source-faithful reading system** for clinical PDFs and photos: + +1. **One visual language** — every page and figure sits in `DocumentFrame` (pixel-faithful + surround, stable loading geometry, announced errors, shared control chrome). +2. **One gesture model** — pinch / modified-wheel zoom, pan when zoomed, fit-width default + on phone; passive scroll by default (#214). +3. **One photo path** — whole-document images and indexed crops/diagrams open the same + lightbox (or an in-frame immersive mode) with shared controls and a11y. +4. **Page ↔ figure sync** — selecting a table/diagram jumps the PDF to its page; optionally + highlights the crop region when geometry exists. +5. **Fast on phone** — no layout jump on signed-URL arrival; deferred image fetch retained; + virtualize long figure rails; keep pdf.js out of the initial document chunk. +6. **Honest dual-engine story** — either promote canvas as the primary reader with native + as progressive enhancement, or hide the toggle behind an advanced preference after + measured parity. + +Design direction stays inside the v2 system (`ckb-v2-tokens.css`, COMPONENTS §6): graded +surround around white clinical pages, hairline frame, no purple/glow/card clutter, no +pixel inversion in any theme. + +--- + +## 3. Hard constraints (non-negotiable) + +1. **One composer owner** — `DocumentViewer` keeps the floating in-doc composer; no second + shell dock on document routes. +2. **Hidden phone reserve = `0rem`** — edge-to-edge footer portal rules stay intact. +3. **Pixel-faithful sources** — no `filter` / `invert` / `color-scheme` on PDF/image pixels + (`document-frame-contract`, COMPONENTS §6). +4. **Auth identity clear** — never re-show a prior user’s signed URLs or detail. +5. **pdf.js on demand** — canvas/native stay out of the initial document client chunk. +6. **Page flips must not remount** the viewer / reload the PDF document. +7. **Private images** — signed URLs, deferred IO, `unoptimized` where bearer URLs require it. +8. **Production tap targets** — `min-h-12` / `min-h-tap` (do not regress to `min-h-11`). +9. **Clinical failures announce** — loading → error must be audible (#219 / LiveAnnouncer). +10. **Not a RAG-ranking change** unless a phase deliberately touches ranking/retrieval — + declare `RAG impact:` accordingly; summarize-from-document hits `/api/answer` so treat + answer-path behaviour changes as clinical governance. + +--- + +## 4. Risk calibration (flightplan vs behaviour) + +Automated flightplan on the proposed paths classified **`ui` only**. + +**Behavioural correction — treat the programme as:** + +| Risk | Why | +| ----------------------------- | ---------------------------------------------------------------- | +| **UI / phone-chrome** | Composer ownership, scroll-hide, DocumentFrame, lightbox Sheet | +| **Clinical document surface** | Source rendering, preview failure UX, pixel fidelity | +| **Privacy** | Signed-URL lifecycle, auth-clear blanking | +| **Not retrieval/RAG ranking** | Unless a later phase changes summarize routing or answer ranking | +| **Not database** | No migration expected for viewer UX | + +PRs that change source rendering / document access need a complete +`## Clinical Governance Preflight` in the PR body even when flightplan says `clinical: false`. + +--- + +## 5. Phased delivery (separate PRs) + +Bundle only when each item is independently low-risk and separately revertible. Prefer +**one phase per PR** for anything that touches gestures, chrome, or PDF raster. + +### Phase 0 — Measurement unblocking (small, own PR) + +**Goal:** fix known INP/a11y debt so redesign work can be measured honestly. + +| Work | Closes | +| -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| Conditional non-passive wheel in `use-viewer-gestures.ts` (passive scroll by default; non-passive only when unmodified wheel-zoom is active) | `#214` | +| Announce PDF canvas + any remaining post-load preview failures (`role=alert` and/or LiveAnnouncer) | `#219` | +| `decoding=async` on lightbox ``; optional `priority` on above-fold `SignedImage` | `#215` (partial) | + +**Verify:** focused gesture + DOM a11y tests; manual INP spot-check on PDF scroll. +**Stop:** do not break Ctrl/⌘+wheel or trackpad pinch zoom. + +### Phase 1 — Shell extraction without UX rewrite + +**Goal:** make `DocumentViewer.tsx` a thin orchestrator so design work is safe. + +Extract (behaviour-preserving): + +- Signed-URL / preview load machine +- Composer + phone chrome wiring +- PDF mode preference + dynamic import boundary +- Summarize stream controller (keep clinical output semantics) + +Clarify `document-viewer-lazy` naming vs real dynamic boundaries. + +**Verify:** shell/DOM contracts, pdf-reader-lazy, client-performance-boundaries, +`document-detail-performance`, `verify:phone-chrome` (dry-run then focused). + +### Phase 2 — Unified viewing chrome (design + approach) + +**Goal:** dramatic visual/UX convergence without changing clinical content. + +1. Deepen `DocumentFrame` adoption toward COMPONENTS §6 — shared zoom/fit/page metadata + chrome for PDF canvas and image stages; keep viewing-aid off by default and forced off + for print/zoomed figures. +2. Route whole-document images through the same immersive viewer as crops (`ImageLightbox` + or in-frame immersive mode sharing gestures). +3. Figure rail → compact filmstrip / gallery with page badges; click syncs PDF page. +4. Intentionally add 2–3 motions (zoom settle, lightbox open, page crossfade) that respect + `prefers-reduced-motion`. +5. Revisit native vs canvas: primary = canvas; native as “browser zoom” advanced option + after parity checklist. + +**Verify:** `document-frame-contract`, document-viewer DOM suites, ui-smoke PDF paths, +`verify:phone-chrome`, a11y focused. Update `docs/search-chrome-behaviour.md` only if +ownership semantics change (prefer not to). + +### Phase 3 — Functionality + optimisation + +**Goal:** make the viewer feel native and stay fast on long documents. + +| Capability | Notes | +| ---------------------------------------- | ------------------------------------------------------------------------------ | +| Multi-page virtualization | Render near pages; dispose far canvases; preserve page URL sync | +| Crop → page overlay | When crop geometry exists, optional highlight on the PDF page | +| Keyboard reading mode | Page Up/Down, `+`/`-`, `f` fit, `r` rotate — documented, tested | +| Rail virtualization | Long `#source-images` lists | +| Smarter signed-URL / decode priority | Above-fold evidence vs below-fold rail | +| Toolbar density | Compact phone / expanded desktop; print chrome hidden via existing print hooks | +| Optional OffscreenCanvas / worker raster | Only after measured main-thread paint cost | + +**Verify:** performance contract tests, bundle budget for document chunk, phone-chrome, +then `verify:ui` at handoff. Lighthouse / live INP needs explicit approval if provider-backed +or production-like. + +### Phase 4 — Polish & design-sync (optional follow-on) + +- Register full `DocumentFrame` in design-sync exports when controls are product-real. +- Capture hosted screenshot baselines for draft→ready adoption. +- Close residual `#215` demo PNG/WebP work only if still warranted. + +--- + +## 6. Suggested file ownership by phase + +### Phase 0 + +- `src/components/document-viewer/use-viewer-gestures.ts` +- `src/components/document-viewer/pdf-canvas-viewer.tsx` +- `src/components/DocumentViewer.tsx` (error announce paths only) +- `src/components/clinical-dashboard/image-lightbox.tsx` +- `src/components/clinical-dashboard/signed-image.tsx` +- Tests: `tests/use-viewer-gestures.dom.test.tsx`, focused DocumentViewer a11y DOM tests + +### Phase 1 + +- `src/components/DocumentViewer.tsx` → new focused modules under `document-viewer/` +- `src/components/document-viewer-lazy.tsx` (naming / real boundary if warranted) +- Existing shell/performance tests + +### Phase 2–3 + +- `src/components/ui/document-frame.tsx` + COMPONENTS §6 / ADOPTION notes as needed +- `pdf-canvas-viewer.tsx`, `non-pdf-source-preview.tsx`, `source-panels.tsx`, + `document-rail-panels.tsx`, `image-lightbox.tsx`, `signed-image.tsx` +- `use-document-viewer-chrome-scroll.ts` only if chrome ownership changes +- `docs/search-chrome-behaviour.md` only if invariants change + +**Correct paths note:** lightbox / signed image live under +`src/components/clinical-dashboard/`, not `src/components/ui/` (flightplan path list +had stale ui/ guesses). + +--- + +## 7. Verification ladder (smallest → widest) + +### Per-phase default (local / offline first) + +```bash +# Gestures / a11y / frame contracts (adjust file list to the phase) +npm run test:focused -- --files \ + tests/use-viewer-gestures.dom.test.tsx,\ + tests/document-viewer-shell.dom.test.tsx,\ + tests/document-viewer-pdf-reader-lazy.test.ts,\ + tests/document-viewer-non-pdf-preview.dom.test.tsx,\ + tests/document-frame-contract.test.ts,\ + tests/document-detail-performance.test.ts,\ + tests/client-performance-boundaries.test.ts,\ + tests/signed-image.dom.test.tsx + +# Before browser work +npm run ensure + +# Phone chrome when composer / reserves / portals touched +npm run verify:phone-chrome -- --dry-run +npm run verify:phone-chrome + +# Handoff +npm run format # commit the result +npm run verify:pr-local -- --dry-run --files +npm run verify:pr-local +``` + +### Widen when warranted + +| Gate | When | +| ------------------------------------ | ---------------------------------------------------------------- | +| `npm run verify:cheap` | Cross-module risk after Phase 1+ extraction or shared primitives | +| `npm run verify:ui` | Shared chrome foundations or PR handoff needing full Chromium | +| `npm run check:production-readiness` | Clinical source-rendering / privacy / env-adjacent phases | + +### Approval-required (do not run without explicit confirmation) + +- Live Lighthouse against production-like hosts +- `verify:release`, `test:live`, `check:supabase-project` +- Any OpenAI-backed summarize regression that is not fully mocked +- Hosted CI reruns / PR mutations outside ordinary push + +Flightplan `approvalRequired` for the proposed path set: **none** (UI-local). That remains +true only while work stays offline/mocked. + +--- + +## 8. Design principles for the visual pass (Phase 2) + +Aligned with repo design rules and COMPONENTS §6: + +- **One composition** on the document route: overview → primary reader → secondary rail; + not a dashboard of competing cards. +- **Brand/clinical quiet:** the page/figure is the hero; chrome is subordinate. +- **Atmosphere from surround grading**, not decorative gradients over pixels. +- **No cards in the reader**; cards only where interaction requires a container (rail + figure actions, error retry). +- **Motion with purpose:** zoom settle, lightbox enter/exit, page change — not noise. +- **Avoid** purple/indigo AI defaults, cream+terracotta clichés, broadsheet density, + glow, emoji, oversized multi-shadow stacks. +- Preserve expressive type already in the v2 token layer; do not introduce Inter/Roboto. + +--- + +## 9. Success criteria + +| Area | Done when | +| ------------- | -------------------------------------------------------------------------------------------------------------------- | +| Design | PDF page and photos share DocumentFrame language; phone and desktop feel intentional | +| Style | Token-only colour/type/space; pixel-faithful content; print chrome clean | +| Approach | Single primary reader path; unified photo immersive path; page↔figure sync | +| Functionality | Keyboard + gesture parity; announced errors; fit-width phone default retained | +| Optimisation | #214 closed; no pdf.js in initial chunk; rail virtualized if long; no remount on page flip; INP scroll path improved | +| Safety | Auth-clear, signed URLs, composer ownership, zero hidden reserve all still green | + +--- + +## 10. Recommended next action + +1. Land this plan doc. +2. Execute **Phase 0** on a dedicated branch (`cursor/viewer-gesture-a11y-…`) — closes + `#214` / `#219` / partial `#215` with focused tests. +3. Then **Phase 1** extraction PR. +4. Only then start **Phase 2** visual redesign with `npm run ensure` + phone-chrome proof + and a Clinical Governance Preflight on the PR. + +Do not start Phase 2 by inventing a second phone composer or changing hide-reserve +semantics. + +--- + +## 11. Residual risk + +- Dual PDF engines may still confuse users until Phase 2 preference policy lands. +- Crop→page overlay depends on crop geometry quality from ingestion (separate worker debt; + do not block viewer UX on perfect crops). +- Summarize-from-document remains a clinical answer path — redesign must not silently + change grounding/fallback behaviour. +- Bundle budget can fail on large visual PRs even when routes are production-critical; + watch `check:bundle-budget` early. +- Flightplan under-classified clinical/privacy behaviour; human PR policy must still + apply governance text when source rendering changes.