ui/phase-26: history mode (turn navigator + read-only banner)

Split GameStateStore into currentTurn (server's latest) and viewedTurn
(displayed snapshot) so history excursions don't corrupt the resume
bookmark or the live-turn bound. Add viewTurn / returnToCurrent /
historyMode rune, plus a game-history cache namespace that stores
past-turn reports for fast re-entry. OrderDraftStore.bindClient takes
a getHistoryMode getter and short-circuits add / remove / move while
the user is viewing a past turn; RenderedReportSource skips the order
overlay in the same case. Header replaces the static "turn N" with a
clickable triplet (TurnNavigator), the layout mounts HistoryBanner
under the header, and visibility-refresh is a no-op while history is
active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-12 00:13:19 +02:00
parent 070fdc0ee5
commit 2d17760a5e
20 changed files with 1572 additions and 118 deletions
@@ -47,6 +47,7 @@ fresh.
import { goto } from "$app/navigation";
import { page } from "$app/state";
import Header from "$lib/header/header.svelte";
import HistoryBanner from "$lib/header/history-banner.svelte";
import Sidebar from "$lib/sidebar/sidebar.svelte";
import BottomTabs from "$lib/sidebar/bottom-tabs.svelte";
import Calculator from "$lib/sidebar/calculator-tab.svelte";
@@ -101,9 +102,6 @@ fresh.
let sidebarOpen = $state(false);
let mobileTool: MobileTool = $state("map");
let activeTab: SidebarTab = $state("inspector");
// Phase 12 ships the prop wiring; Phase 26 replaces this constant
// with the real history-mode signal from `lib/history-mode.ts`.
const historyMode = false;
const gameId = $derived(page.params.id ?? "");
const isOnMap = $derived(/\/games\/[^/]+\/map\/?$/.test(page.url.pathname));
@@ -115,6 +113,13 @@ fresh.
setContext(GAME_STATE_CONTEXT_KEY, gameState);
const orderDraft = new OrderDraftStore();
setContext(ORDER_DRAFT_CONTEXT_KEY, orderDraft);
// Phase 26: the order tab vanishes from the sidebar and bottom-tabs
// when the player is viewing a past turn. The flag is owned by
// `GameStateStore` (single source of truth for "what turn are we
// looking at") and surfaced here so the Phase 12 sidebar wiring,
// the new `HistoryBanner`, and `orderDraft.bindClient` all read
// from the same derivation.
const historyMode = $derived(gameState.historyMode);
const selection = new SelectionStore();
setContext(SELECTION_CONTEXT_KEY, selection);
const renderedReport = createRenderedReportSource(gameState, orderDraft);
@@ -398,6 +403,7 @@ fresh.
galaxyClient.set(client);
orderDraft.bindClient(client, {
getCurrentTurn: () => gameState.currentTurn,
getHistoryMode: () => gameState.historyMode,
});
// The server is always polled at game boot — its
// stored order may be fresher than the local cache
@@ -441,6 +447,7 @@ fresh.
{sidebarOpen}
onToggleSidebar={toggleSidebar}
/>
<HistoryBanner />
<div class="body">
<main class="active-view-host" data-testid="active-view-host">
{#if effectiveTool === "calc"}