e31fb2c17a
Tests · UI / test (push) Failing after 9m28s
Rewrite ui/docs (navigation, order-composer, auth-flow, pwa-strategy, game-state + secondary topic docs) and ui/README for the single-URL app-shell (in-memory screens/views, Back→lobby via shallow routing, sessionStorage restore + validation, return-to-lobby). ui/PLAN.md gets a Phase-10 supersede note (implemented; standalone-compatible). Fix stale code comments (session-store auth gate, report-sections spec contract). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
170 lines
8.2 KiB
Markdown
170 lines
8.2 KiB
Markdown
# 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 <select>
|
|
├── report/section-galaxy-summary.svelte
|
|
├── report/section-votes.svelte
|
|
├── report/section-player-status.svelte
|
|
├── report/section-my-sciences.svelte
|
|
├── report/section-foreign-sciences.svelte
|
|
├── report/section-my-ship-classes.svelte
|
|
├── report/section-foreign-ship-classes.svelte
|
|
├── report/section-battles.svelte
|
|
├── report/section-bombings.svelte
|
|
├── report/section-approaching-groups.svelte
|
|
├── report/section-my-planets.svelte
|
|
├── report/section-ships-in-production.svelte
|
|
├── report/section-cargo-routes.svelte
|
|
├── report/section-foreign-planets.svelte
|
|
├── report/section-uninhabited-planets.svelte
|
|
├── report/section-unknown-planets.svelte
|
|
├── report/section-my-fleets.svelte
|
|
├── report/section-my-ship-groups.svelte
|
|
├── report/section-foreign-ship-groups.svelte
|
|
└── report/section-unidentified-groups.svelte
|
|
```
|
|
|
|
Each section component is self-contained:
|
|
- reads `RenderedReportSource` from context;
|
|
- renders the loading copy when `rendered.report === null`;
|
|
- renders the empty-state copy when its array is empty;
|
|
- otherwise emits a `<section id="report-<slug>" data-testid="report-section-<slug>">`
|
|
containing the relevant grid / list / kv-list.
|
|
|
|
No shared `<Section>` wrapper exists. The visual scaffolding (dark
|
|
grid CSS, header style, status paragraph) is inlined per
|
|
component. The CLAUDE.md "wait for the third real caller before
|
|
extracting an abstraction" rule applies; with one shape per
|
|
section, the per-section inline CSS is the smallest correct
|
|
solution.
|
|
|
|
Shared formatters live in `report/format.ts` (`formatPercent`,
|
|
`formatCount`, `formatFloat`, `formatVotes`, `planetLabel`).
|
|
|
|
## Section order, data sources, empty copy
|
|
|
|
| # | Slug | Data | Empty copy (en) |
|
|
|---|------|------|-----------------|
|
|
| 1 | `galaxy-summary` | header turn / size / planet count / race | never empty |
|
|
| 2 | `votes` | `myVotes`, `myVoteFor`, `races[].votesReceived` | "no votes cast yet" |
|
|
| 3 | `player-status` | `players[]` (full roster, self + extinct) | never empty |
|
|
| 4 | `my-sciences` | `localScience[]` | "no sciences defined yet" |
|
|
| 5 | `foreign-sciences` | `otherScience[]`, one sub-table per race | "no foreign sciences observed yet" |
|
|
| 6 | `my-ship-classes` | `localShipClass[]` | "no ship classes designed yet" |
|
|
| 7 | `foreign-ship-classes` | `otherShipClass[]`, one sub-table per race | "no foreign ship classes observed yet" |
|
|
| 8 | `battles` | `battleIds[]` (inactive monospace spans) | "no battles last turn" |
|
|
| 9 | `bombings` | `bombings[]`, wiped rows visually distinct | "no bombings last turn" |
|
|
| 10 | `approaching-groups` | `incomingShipGroups[]` | "no approaching groups" |
|
|
| 11 | `my-planets` | `planets.filter(kind==="local")` | "no planets owned yet" |
|
|
| 12 | `ships-in-production` | `shipProductions[]` | "no ships in production" |
|
|
| 13 | `cargo-routes` | `routes[]` (flattened to one row per entry) | "no cargo routes set" |
|
|
| 14 | `foreign-planets` | `planets.filter(kind==="other")` | "no foreign planets observed" |
|
|
| 15 | `uninhabited-planets` | `planets.filter(kind==="uninhabited")` | "no uninhabited planets observed" |
|
|
| 16 | `unknown-planets` | `planets.filter(kind==="unidentified")` | "no unknown planets" |
|
|
| 17 | `my-fleets` | `localFleets[]` | "no fleets created yet" |
|
|
| 18 | `my-ship-groups` | `localShipGroups[]` | "no ship groups yet" |
|
|
| 19 | `foreign-ship-groups` | `otherShipGroups[]` | "no foreign ship groups observed" |
|
|
| 20 | `unidentified-groups` | `unidentifiedShipGroups[]` | "no unidentified groups" |
|
|
|
|
The orchestrator iterates this list once for the TOC and once for
|
|
the body — both surfaces stay in sync by construction.
|
|
|
|
## Table of contents and active highlight
|
|
|
|
`report/report-toc.svelte` renders two surfaces driven by the same
|
|
entry list:
|
|
|
|
- **Desktop / tablet sidebar** — sticky `<aside>` with vertical
|
|
anchor list. The anchor for the currently-visible section gets
|
|
`aria-current="location"` and an `.active` CSS class.
|
|
- **Mobile (< 768 px)** — the desktop sidebar is hidden via CSS
|
|
and a sticky `<select>` 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 `<main class="active-view-host">` 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.<slug>.title` — section heading.
|
|
- `game.report.section.<slug>.empty` — empty-state copy (where
|
|
applicable).
|
|
- `game.report.section.<slug>.column.<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 `<select>` scrolls
|
|
to the chosen section on a narrow viewport. The spec drives the
|
|
app-shell through `window.__galaxyNav` (the dev-only nav surface)
|
|
instead of `page.goto` per-view URLs. The old "scroll position
|
|
survives a `/map` round-trip via SvelteKit `Snapshot`" case was
|
|
dropped — see the [scroll position](#scroll-position) note.
|
|
|
|
Test IDs follow the pattern `report-section-<slug>` for section
|
|
roots, `report-toc-<slug>` for TOC anchors, and per-section row
|
|
identifiers (e.g. `report-bombing-row`, `my-planets-row`).
|