cfbe052242
- Replace the 14 rem sticky sidebar (and its mobile <select> twin)
with a single sticky icon-popup trigger pinned to the top-right
corner of the report column. Trigger shows `≡` followed by the
currently active section title (CSS-clamped with text-overflow:
ellipsis so long RU titles cannot bloat the button). Click opens
an anchored popover on desktop and a fixed bottom-sheet on
<768.98 px (mirrors lib/active-view/map-toggles.svelte).
- Each menuitem closes the popover and scrolls the matching
`<section id="report-<slug>">` into view. The scroll is deferred
one animation frame so the surface unmount + restoreFocus's
focus restoration on the (sticky) trigger commit first; otherwise
the focus call could cancel the just-started smooth/instant
scroll under desktop Chromium and WebKit.
- Drop the in-report "Back to map" button — the same affordance
lives in the app-shell view menu (tests/e2e/game-shell.spec.ts
covers it).
- Tighten the report grid to a single flex column so the section
body now occupies the full container width.
- i18n: remove game.report.back_to_map and
game.report.toc.mobile_label; add game.report.toc.open and
game.report.toc.close (mirrors game.map.toggles.open/close).
- Tests: Vitest report-toc.test.ts rewritten for the new icon-popup
contract; Playwright report-sections.spec.ts switches the anchor
loop to trigger → menuitem and adds a mobile bottom-sheet
assertion; game-shell-stubs.test.ts no longer asserts the
back-to-map button on the report orchestrator.
- Docs: ui/docs/report-view.md (TOC + i18n + test seams) and
docs/FUNCTIONAL{,_ru}.md §6.4 updated. The stale SvelteKit
Snapshot reference (the route file was removed by the single-URL
app-shell) is dropped at the same time.
Refs: #52 (#43 umbrella).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
155 lines
6.5 KiB
Svelte
155 lines
6.5 KiB
Svelte
<!--
|
|
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 lands here: an `IntersectionObserver`
|
|
rooted on the viewport watches every `<section id="report-<slug>">`
|
|
and updates a local `activeSlug` rune that drives the TOC highlight.
|
|
|
|
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 { onMount } from "svelte";
|
|
|
|
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" },
|
|
];
|
|
|
|
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>
|
|
|
|
<div class="report-view" data-testid="active-view-report">
|
|
<ReportToc entries={ENTRIES} {activeSlug} />
|
|
|
|
<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>
|
|
.report-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 1rem 1.25rem 2rem;
|
|
font-family: system-ui, sans-serif;
|
|
}
|
|
.report-body {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.75rem;
|
|
}
|
|
@media (max-width: 767.98px) {
|
|
.report-view {
|
|
padding: 0.75rem;
|
|
}
|
|
}
|
|
</style>
|