# Report view — Phase 23 The Phase 23 in-game "turn report" view is a single scrollable layout with twenty sections, one per array on the FBS `Report` table. The route file is the standard two-line wrapper; the orchestrator and the per-section components live under `ui/frontend/src/lib/active-view/report/`. ## Component layout `lib/active-view/report.svelte` is the orchestrator. It owns the section list, instantiates `IntersectionObserver` to track which section is active, and renders the table of contents alongside the section column. ``` report.svelte ├── report/report-toc.svelte // anchor list + mobile ` takes its place at the top of the body. Picking an option scrolls the matching section into view. The mobile contract intentionally avoids stacking another overlay on top of the existing layout-owned bottom-tabs. Both surfaces also expose a "Back to map" affordance (`report-back-to-map`) at the top. The active slug is computed by an `IntersectionObserver` rooted on the viewport (`root: null`) with `rootMargin: "-30% 0px -60% 0px"`. The skew biases the active band toward the upper third of the visible area so that scrolling down advances the highlight naturally. The observer is created on mount and torn down on unmount. The in-game shell layout (`routes/games/[id]/+layout.svelte`) expands `
` to fit content rather than constraining it, so the document body is the actual scroll container — not the host. The IntersectionObserver root is `null` to match. ## Scroll save / restore `routes/games/[id]/report/+page.svelte` exports a SvelteKit `Snapshot<{ scrollY: number }>`: - `capture()` reads `window.scrollY` when SvelteKit's `beforeNavigate` cycle runs. - `restore(value)` schedules a short `requestAnimationFrame` poll that waits for `document.documentElement.scrollHeight` to grow tall enough to honour the saved offset, then calls `window.scrollTo(0, value)`. The poll caps at ~60 frames (≈ 1 second) so a never-tall-enough body never pins a frame loop. The capture / restore pair is keyed by route, so: - Forward navigation from `/report` to `/map` lands `/map` at scrollY 0 (no snapshot for `/map` to restore from). - History-back from `/map` to `/report` restores the previously captured scrollY — the user returns to the same section. The Snapshot API does not capture the active sidebar slug; the IntersectionObserver re-derives it from the restored scroll position on the next animation frame, which keeps the TOC highlight consistent without a second source of truth. ## i18n namespace All Phase 23 strings live under `game.report.*`: - `game.report.loading` — section loading placeholder. - `game.report.back_to_map`, `game.report.toc.title`, `game.report.toc.mobile_label` — shell-level strings. - `game.report.section..title` — section heading. - `game.report.section..empty` — empty-state copy (where applicable). - `game.report.section..column.` — column headings. - A small number of section-specific keys (`bombings.wiped`, `player_status.local_marker`, `player_status.extinct_marker`, `foreign_sciences.race_header`, `foreign_ship_classes.race_header`, `battles.id_label`, `votes.target_none`). The namespace is intentionally separate from `game.table.*` even where the data shape overlaps (e.g. sciences, ship classes); the two surfaces evolve independently and a shared key set would couple them silently. ## Test seams - **Vitest** — four representative specs cover the four section shapes: kv-list (`report-section-galaxy-summary.test.ts`), grid with conditional row state (`report-section-bombings.test.ts`), per-race sub-table (`report-section-foreign-sciences.test.ts`), TOC (`report-toc.test.ts`). Each spec mounts the component against a synthetic `RenderedReportSource`, so the orchestrator / IntersectionObserver are out of scope. - **Playwright** — `tests/e2e/report-sections.spec.ts` exercises the full integration: every TOC anchor lands its section in view, the snapshot mechanism preserves `window.scrollY` on history navigation, the back-to-map button reaches `/map`, the mobile `