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:
@@ -1,28 +1,181 @@
|
||||
<!--
|
||||
Phase 10 stub for the turn-report active view. Phase 23 replaces the
|
||||
body with the per-turn sections (cargo deliveries, completed sciences,
|
||||
mail, etc.).
|
||||
Phase 23 turn-report active view.
|
||||
|
||||
Composes the table of contents (`report/report-toc.svelte`) and the
|
||||
twenty section components that render each `GameReport` array. Each
|
||||
section is its own component under `lib/active-view/report/` — the
|
||||
data shapes are too varied for one generic table, and the
|
||||
component-per-section seam matches Phase 23's targeted-test contract.
|
||||
|
||||
Active-section highlighting and scroll save/restore land here:
|
||||
- `IntersectionObserver` rooted on the active-view-host element
|
||||
(`bind:this` in `+layout.svelte`, plumbed through
|
||||
`ACTIVE_VIEW_HOST_CONTEXT_KEY`) watches every `<section
|
||||
id="report-<slug>">` and updates a local `activeSlug` rune.
|
||||
- The matching `+page.svelte` exports a SvelteKit `Snapshot` that
|
||||
captures and restores `host.element.scrollTop`, so navigating to
|
||||
/map and back lands on the same scroll position. The save lives in
|
||||
`+page.svelte` because SvelteKit binds snapshots per route.
|
||||
|
||||
The 20-section list lives here as a single source of truth so the
|
||||
TOC and the body iterate the same data.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
|
||||
import ReportToc, {
|
||||
type TocEntry,
|
||||
} from "./report/report-toc.svelte";
|
||||
import SectionGalaxySummary from "./report/section-galaxy-summary.svelte";
|
||||
import SectionVotes from "./report/section-votes.svelte";
|
||||
import SectionPlayerStatus from "./report/section-player-status.svelte";
|
||||
import SectionMySciences from "./report/section-my-sciences.svelte";
|
||||
import SectionForeignSciences from "./report/section-foreign-sciences.svelte";
|
||||
import SectionMyShipClasses from "./report/section-my-ship-classes.svelte";
|
||||
import SectionForeignShipClasses from "./report/section-foreign-ship-classes.svelte";
|
||||
import SectionBattles from "./report/section-battles.svelte";
|
||||
import SectionBombings from "./report/section-bombings.svelte";
|
||||
import SectionApproachingGroups from "./report/section-approaching-groups.svelte";
|
||||
import SectionMyPlanets from "./report/section-my-planets.svelte";
|
||||
import SectionShipsInProduction from "./report/section-ships-in-production.svelte";
|
||||
import SectionCargoRoutes from "./report/section-cargo-routes.svelte";
|
||||
import SectionForeignPlanets from "./report/section-foreign-planets.svelte";
|
||||
import SectionUninhabitedPlanets from "./report/section-uninhabited-planets.svelte";
|
||||
import SectionUnknownPlanets from "./report/section-unknown-planets.svelte";
|
||||
import SectionMyFleets from "./report/section-my-fleets.svelte";
|
||||
import SectionMyShipGroups from "./report/section-my-ship-groups.svelte";
|
||||
import SectionForeignShipGroups from "./report/section-foreign-ship-groups.svelte";
|
||||
import SectionUnidentifiedGroups from "./report/section-unidentified-groups.svelte";
|
||||
|
||||
const ENTRIES: readonly TocEntry[] = [
|
||||
{ slug: "galaxy-summary", titleKey: "game.report.section.galaxy_summary.title" },
|
||||
{ slug: "votes", titleKey: "game.report.section.votes.title" },
|
||||
{ slug: "player-status", titleKey: "game.report.section.player_status.title" },
|
||||
{ slug: "my-sciences", titleKey: "game.report.section.my_sciences.title" },
|
||||
{ slug: "foreign-sciences", titleKey: "game.report.section.foreign_sciences.title" },
|
||||
{ slug: "my-ship-classes", titleKey: "game.report.section.my_ship_classes.title" },
|
||||
{ slug: "foreign-ship-classes", titleKey: "game.report.section.foreign_ship_classes.title" },
|
||||
{ slug: "battles", titleKey: "game.report.section.battles.title" },
|
||||
{ slug: "bombings", titleKey: "game.report.section.bombings.title" },
|
||||
{ slug: "approaching-groups", titleKey: "game.report.section.approaching_groups.title" },
|
||||
{ slug: "my-planets", titleKey: "game.report.section.my_planets.title" },
|
||||
{ slug: "ships-in-production", titleKey: "game.report.section.ships_in_production.title" },
|
||||
{ slug: "cargo-routes", titleKey: "game.report.section.cargo_routes.title" },
|
||||
{ slug: "foreign-planets", titleKey: "game.report.section.foreign_planets.title" },
|
||||
{ slug: "uninhabited-planets", titleKey: "game.report.section.uninhabited_planets.title" },
|
||||
{ slug: "unknown-planets", titleKey: "game.report.section.unknown_planets.title" },
|
||||
{ slug: "my-fleets", titleKey: "game.report.section.my_fleets.title" },
|
||||
{ slug: "my-ship-groups", titleKey: "game.report.section.my_ship_groups.title" },
|
||||
{ slug: "foreign-ship-groups", titleKey: "game.report.section.foreign_ship_groups.title" },
|
||||
{ slug: "unidentified-groups", titleKey: "game.report.section.unidentified_groups.title" },
|
||||
];
|
||||
|
||||
const gameId = $derived(page.params.id ?? "");
|
||||
|
||||
let activeSlug = $state<string>(ENTRIES[0]?.slug ?? "");
|
||||
let bodyEl: HTMLDivElement | null = $state(null);
|
||||
|
||||
// `IntersectionObserver` rooted on the viewport (`root: null`)
|
||||
// lets the TOC highlight follow the section currently in the
|
||||
// upper portion of the visible area. The in-game shell layout
|
||||
// expands the active-view-host to fit content rather than
|
||||
// constraining it, so the document body scrolls — not the host.
|
||||
// Targeting the viewport with a top-skewed `rootMargin` advances
|
||||
// the highlight as a section enters the upper third of what the
|
||||
// reader sees, without coupling to the layout's internal sizing.
|
||||
onMount(() => {
|
||||
if (typeof IntersectionObserver === "undefined") return;
|
||||
const body = bodyEl;
|
||||
if (body === null) return;
|
||||
const targets = body.querySelectorAll<HTMLElement>("section[id^='report-']");
|
||||
if (targets.length === 0) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
let pick: { slug: string; ratio: number } | null = null;
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
const slug = entry.target.id.replace(/^report-/, "");
|
||||
if (pick === null || entry.intersectionRatio > pick.ratio) {
|
||||
pick = { slug, ratio: entry.intersectionRatio };
|
||||
}
|
||||
}
|
||||
if (pick !== null) {
|
||||
activeSlug = pick.slug;
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: "-30% 0px -60% 0px",
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1],
|
||||
},
|
||||
);
|
||||
targets.forEach((t) => observer.observe(t));
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="active-view" data-testid="active-view-report">
|
||||
<h2>{i18n.t("game.view.report")}</h2>
|
||||
<p>{i18n.t("game.shell.coming_soon")}</p>
|
||||
</section>
|
||||
<div class="report-view" data-testid="active-view-report">
|
||||
<ReportToc entries={ENTRIES} {activeSlug} {gameId} />
|
||||
|
||||
<div class="report-body" bind:this={bodyEl}>
|
||||
<SectionGalaxySummary />
|
||||
<SectionVotes />
|
||||
<SectionPlayerStatus />
|
||||
<SectionMySciences />
|
||||
<SectionForeignSciences />
|
||||
<SectionMyShipClasses />
|
||||
<SectionForeignShipClasses />
|
||||
<SectionBattles />
|
||||
<SectionBombings />
|
||||
<SectionApproachingGroups />
|
||||
<SectionMyPlanets />
|
||||
<SectionShipsInProduction />
|
||||
<SectionCargoRoutes />
|
||||
<SectionForeignPlanets />
|
||||
<SectionUninhabitedPlanets />
|
||||
<SectionUnknownPlanets />
|
||||
<SectionMyFleets />
|
||||
<SectionMyShipGroups />
|
||||
<SectionForeignShipGroups />
|
||||
<SectionUnidentifiedGroups />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.active-view {
|
||||
padding: 1.5rem;
|
||||
.report-view {
|
||||
display: grid;
|
||||
grid-template-columns: 14rem 1fr;
|
||||
gap: 1.25rem;
|
||||
padding: 1rem 1.25rem 2rem;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
.active-view h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
.report-view > :global(.report-toc) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
padding: 0.5rem 0;
|
||||
max-height: calc(100vh - 4rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.active-view p {
|
||||
margin: 0;
|
||||
color: #555;
|
||||
.report-body {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.report-view {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0.75rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.report-view > :global(.report-toc) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #0a0e1a;
|
||||
padding: 0.5rem 0;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user