# Phase 26 — Document Modal Viewer **Source:** `TODO.md L4 — "New documents should open in an almost-fullscreen modal, not in a new page"` **Story:** `.agent/user_stories/document-modal.md` **Context:** Phase 10 added the separate `/document.html` viewer page; phase 19 added the shared header bar that now lives on every page. The document content is served by the stateless `GET /api/documents/content` endpoint (PLAN §4). ## Objective Stop opening cited documents in a new page/tab. Clicking a source chip or a Sources-table path link now opens the document in an **almost-fullscreen modal overlay** on the current page, fed by the same `/api/documents/content` endpoint. The existing `/document.html` page stays as the no-JS / direct-link fallback and its behaviour is unchanged. ## Dependencies - `10_story_document_viewer` (complete) — the `/document.html` page, the `document.js` renderer, the `renderMarkdown` escape-first renderer in `markdown.js`, and the `#doc-content` / `.doc-md` / `.doc-raw` markup this phase reuses inside the modal. - `19_shared_header` (complete) — the shared header bar the modal sits under; the modal must not disturb the header. - `08_story_dark_tech_theme` (complete) — the Phase-08 tokens and the ≥4.5:1 contrast / `prefers-reduced-motion` contract the modal must honour. ## Tasks 1. `01_modal_css_and_html.md` — the modal CSS (overlay, backdrop, close button, scrollable content area) + inject the modal skeleton into `index.html` 2. `02_app_js_modal_intercept.md` — intercept document links in `app.js` + `sources.js`, fetch content via `/api/documents/content`, render inside the modal 3. `03_document_js_modal_mode.md` — adapt `document.js` to optionally render in modal mode (reuse the same API call) for the direct-link fallback path 4. `04_e2e_regression_suite.md` — update `test_document_viewer.py` to verify modal behaviour; the story gate, run in isolation ## Testing & Quality - Unit/integration: none required for the modal itself (frontend-only); the `/api/documents/content` endpoint is unchanged (no `app/` change → no coverage delta). - Coverage: frontend-only; the >90% `app/` gate is unaffected. - E2E: `tests/e2e/test_document_viewer.py` rewritten for the modal contract (task 4), green **in isolation** (prereq `podman compose up -d db`). ## Completion Criteria - [ ] Clicking a source chip (chat) or a Sources-table path link opens the document in an almost-fullscreen modal on the **same page** (no new tab, no navigation). - [ ] The modal renders the same content the `/document.html` page renders: md/markdown via the shared renderer (`.doc-md`), other formats in `
`, source/format/path/indexed/chunks meta. - [ ] The modal has a visible close control, closes on Escape, closes on backdrop click, and keeps the dark theme + a11y frame (skip-link, focus trap, `:focus-visible`, aria-label). - [ ] The existing `/document.html` page still works unchanged (direct link, back button, XSS-safe rendering, not-found state). - [ ] No CDN tags on any touched page; every asset reference is same-origin or `data:`. - [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL ≥ pre-change number (gate >90%). - [ ] `uv run pytest tests/e2e/test_document_viewer.py -v --no-cov` green in isolation. - [ ] `uv run ruff check . && uv run pyright` clean. - [ ] UI Structure Check (AGENTS.md rule 5): modal content uses the standard centered column width for md; backdrop behind content; no 360px overflow. - [ ] `.agent/user_stories/document-modal.md` exists. - [ ] One `--no-gpg-sign` commit staging only this phase's files; `.agent/phases/todo/26_document_modal_viewer/` moved to `.agent/phases/complete/`. ## Locked decisions - **No backend change** — the modal reuses `GET /api/documents/content` unchanged (A10 untouched: the API stays stateless). - **A11 untouched** — vanilla HTML/CSS/JS, no CDN, zero new packages, no new assets, system font stack; the modal is pure CSS + JS. - **No anchor revised** — this is a UI-behaviour change (PLAN §7.5 gains `#doc-modal`, `#doc-modal-backdrop`, `#doc-modal-close`, `#doc-modal-content`); the `/document.html` page and its story are unchanged. - **A16 honoured** — one story E2E suite (rewritten) + adapted regressions. - **A17 honoured** — one atomic `--no-gpg-sign` commit.