# Report view The 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 (`lib/game/game-shell.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 position The report is the `report` active view; switching to another view is an in-memory `activeView` state change, not a navigation, and the report component is remounted when the user returns to it. The single-URL app-shell therefore does not carry SvelteKit's route-keyed `Snapshot` scroll save/restore — that mechanism was tied to the old `/games/:id/report` route and was removed with it. A re-entered report opens at the top; the IntersectionObserver re-derives the active TOC slug from the scroll position on the next animation frame, so the highlight stays consistent without a separate source of truth. ## i18n namespace All 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 back-to-map button switches to the map view (`activeView.select("map")`), and the mobile `