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>
8.2 KiB
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
RenderedReportSourcefrom 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 getsaria-current="location"and an.activeCSS 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 syntheticRenderedReportSource, so the orchestrator / IntersectionObserver are out of scope. - Playwright —
tests/e2e/report-sections.spec.tsexercises 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 throughwindow.__galaxyNav(the dev-only nav surface) instead ofpage.gotoper-view URLs. The old "scroll position survives a/mapround-trip via SvelteKitSnapshot" case was dropped — see the 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).