ui/phase-23: turn-report view with twenty sections and TOC

Replaces the Phase 10 report stub with a scrollable orchestrator that
renders every FBS array as a dedicated section (galaxy summary, votes,
player status, my/foreign sciences, my/foreign ship classes, battles,
bombings, approaching groups, my/foreign/uninhabited/unknown planets,
ships in production, cargo routes, my fleets, my/foreign/unidentified
ship groups). A sticky table of contents (a <select> on mobile),
"back to map" affordance, IntersectionObserver-driven active-section
highlight, and SvelteKit Snapshot-based scroll save/restore round out
the view.

GameReport gains six new fields (players, otherScience, otherShipClass,
battleIds, bombings, shipProductions); decodeReport, the synthetic-
report loader, the e2e fixture builder, and EMPTY_SHIP_GROUPS extend
in lockstep. ~90 new i18n keys land in en + ru together.

The legacy-report parser is extended to populate the new sections from
the dg/gplus text formats (Your Sciences, <Race> Sciences, <Race> Ship
Types, Bombings, Ships In Production). Ships-in-production prod_used
is derived through a new pkg/calc.ShipBuildCost helper; the engine's
controller.ProduceShip refactors to call the same helper without any
behaviour change (engine tests stay unchanged and green). Battles
remain in the parser's Skipped list — the legacy text carries no
stable per-battle UUID.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-11 14:33:56 +02:00
parent 81d8be08b2
commit c58027c034
48 changed files with 5368 additions and 103 deletions
@@ -0,0 +1,91 @@
<!--
Phase 23 Report View — battles section. The wire only carries
battle UUIDs (the full battle report is fetched lazily by Phase 27),
so each row is a monospace, non-interactive `<span>` of the battle
identifier. Phase 27 will turn each row into a link to
`/games/<id>/battle/<uuid>`; until then dead links are worse than
plain text.
-->
<script lang="ts">
import { getContext } from "svelte";
import { i18n } from "$lib/i18n/index.svelte";
import {
RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource,
} from "$lib/rendered-report.svelte";
const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
);
const report = $derived(rendered?.report ?? null);
const ids = $derived(report?.battleIds ?? []);
</script>
<section
id="report-battles"
class="grid-section"
data-testid="report-section-battles"
>
<h2>{i18n.t("game.report.section.battles.title")}</h2>
{#if report === null}
<p class="status">{i18n.t("game.report.loading")}</p>
{:else if ids.length === 0}
<p class="status" data-testid="battles-empty">
{i18n.t("game.report.section.battles.empty")}
</p>
{:else}
<ul class="ids" data-testid="battles-list">
{#each ids as id (id)}
<li>
<span class="label">
{i18n.t("game.report.section.battles.id_label")}
</span>
<span
class="uuid"
data-testid="report-battle-row"
data-id={id}
>{id}</span>
</li>
{/each}
</ul>
{/if}
</section>
<style>
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
}
.status {
margin: 0;
color: #888;
font-size: 0.9rem;
}
.ids {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.85rem;
}
.ids li {
display: flex;
align-items: baseline;
gap: 0.6rem;
}
.label {
color: #aab;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.7rem;
}
.uuid {
color: #cfd7ff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
</style>