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:
@@ -148,6 +148,7 @@ export class OrderDraftStore {
|
||||
private queue = new OrderQueue();
|
||||
private queueStarted = false;
|
||||
private getCurrentTurn: (() => number) | null = null;
|
||||
private getHistoryMode: (() => boolean) | null = null;
|
||||
|
||||
/**
|
||||
* init loads the persisted draft for `opts.gameId` from `opts.cache`
|
||||
@@ -195,13 +196,24 @@ export class OrderDraftStore {
|
||||
* interpolate the turn number the player was composing for. The
|
||||
* layout passes `() => gameState.currentTurn`; tests may omit it,
|
||||
* in which case the banner falls back to a turn-less template.
|
||||
*
|
||||
* Phase 26: `opts.getHistoryMode` lets `add` / `remove` / `move`
|
||||
* short-circuit while the user is viewing a past turn. Without
|
||||
* the gate, inspector affordances built in Phases 14–22 would
|
||||
* happily push commands into the draft even though the order tab
|
||||
* is hidden and the read-only banner is visible. Tests may omit
|
||||
* it; the default is "never in history mode".
|
||||
*/
|
||||
bindClient(
|
||||
client: GalaxyClient,
|
||||
opts: { getCurrentTurn?: () => number } = {},
|
||||
opts: {
|
||||
getCurrentTurn?: () => number;
|
||||
getHistoryMode?: () => boolean;
|
||||
} = {},
|
||||
): void {
|
||||
this.client = client;
|
||||
this.getCurrentTurn = opts.getCurrentTurn ?? null;
|
||||
this.getHistoryMode = opts.getHistoryMode ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,6 +317,11 @@ export class OrderDraftStore {
|
||||
*/
|
||||
async add(command: OrderCommand): Promise<void> {
|
||||
if (this.status !== "ready") return;
|
||||
// Phase 26: history mode hides the order tab and treats every
|
||||
// view as read-only. The inspector affordances are not aware of
|
||||
// the mode, so the gate lives here — one chokepoint protects
|
||||
// every Phase 14–22 caller without per-component edits.
|
||||
if (this.getHistoryMode?.() === true) return;
|
||||
this.clearConflictForMutation();
|
||||
const removed: string[] = [];
|
||||
let nextCommands: OrderCommand[];
|
||||
@@ -385,6 +402,7 @@ export class OrderDraftStore {
|
||||
*/
|
||||
async remove(id: string): Promise<void> {
|
||||
if (this.status !== "ready") return;
|
||||
if (this.getHistoryMode?.() === true) return;
|
||||
const next = this.commands.filter((cmd) => cmd.id !== id);
|
||||
if (next.length === this.commands.length) return;
|
||||
this.clearConflictForMutation();
|
||||
@@ -406,6 +424,7 @@ export class OrderDraftStore {
|
||||
*/
|
||||
async move(fromIndex: number, toIndex: number): Promise<void> {
|
||||
if (this.status !== "ready") return;
|
||||
if (this.getHistoryMode?.() === true) return;
|
||||
const length = this.commands.length;
|
||||
if (fromIndex < 0 || fromIndex >= length) return;
|
||||
if (toIndex < 0 || toIndex >= length) return;
|
||||
@@ -479,6 +498,7 @@ export class OrderDraftStore {
|
||||
this.cache = null;
|
||||
this.client = null;
|
||||
this.getCurrentTurn = null;
|
||||
this.getHistoryMode = null;
|
||||
if (this.queueStarted) {
|
||||
this.queue.stop();
|
||||
this.queueStarted = false;
|
||||
|
||||
Reference in New Issue
Block a user