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
+34 -5
View File
@@ -75,17 +75,46 @@ end-to-end command flow) can set it on navigation.
The Order entry is hidden when the layout's `historyMode` flag is
true. Phase 12 plumbs the flag end-to-end as a prop —
`+layout.svelte` passes a constant `false` to `Sidebar`, which
`+layout.svelte` forwards a derived value to `Sidebar`, which
forwards `hideOrder` to its `TabBar`; the same flag goes to
`BottomTabs` so the mobile `Order` button is also suppressed. A
`?sidebar=order` URL seed that arrives while the flag is true falls
back to `inspector`, and an `$effect` on the sidebar resets
`activeTab` away from `order` if the flag flips on mid-session.
Phase 26 introduces `lib/history-mode.ts` and replaces the constant
with the live signal; the order draft survives the toggle because
Phase 26 wires the flag to the live history signal owned by
`GameStateStore`. The derivation lives directly in `+layout.svelte`
(`const historyMode = $derived(gameState.historyMode)`) — no
separate `lib/history-mode.ts` module ships, because the layout is
the single consumer and the project's compactness rule rejects a
one-line indirection. The order draft survives the toggle because
`OrderDraftStore` lives one level above the sidebar in the layout
hierarchy. See [`order-composer.md`](order-composer.md) for the
draft-store side of the flow.
hierarchy; the same `historyMode` derivation is also fed into
`OrderDraftStore.bindClient` so inspector-driven mutations
(`add` / `remove` / `move`) become no-ops while the user is
viewing a past turn. See [`order-composer.md`](order-composer.md)
for the draft-store side of the flow and
[`game-state.md`](game-state.md) for the rune split between
`currentTurn` and `viewedTurn`.
## Header turn navigator and history banner
The header replaces the Phase 11 inline `turn N` text with a
`← Turn N →` triplet (`lib/header/turn-navigator.svelte`). The
arrows step `viewedTurn` by ±1 (disabled at boundaries `0` and
`currentTurn`); clicking the middle button opens an absolute
popover (desktop) or a fixed full-width drawer (mobile, ≤ 767.98
px) listing every turn from `currentTurn` down to `0`. Selecting
the current-turn row routes through `gameState.returnToCurrent()`;
any other row calls `gameState.viewTurn(N)`. The popover reuses
`view-menu.svelte`'s outside-click / Escape pattern.
`lib/header/history-banner.svelte` renders directly under the
header whenever `gameState.historyMode === true`. It shows
"Viewing turn {N} · read-only" with a "Return to current turn"
button that delegates back to `gameState.returnToCurrent()`. Both
the navigator and the banner read `gameState` through context, so
the layout is the only place where the wiring lives.
## Layout breakpoints