Add undo/redo for all encounter actions
Memento-based undo/redo with full encounter snapshots. Undo stack capped at 50 entries, persisted to localStorage. Triggered via buttons in the top bar (inboard of turn navigation) and keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z, Cmd on Mac, case-insensitive key matching). Clear encounter resets both stacks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
70
packages/domain/src/undo-redo.ts
Normal file
70
packages/domain/src/undo-redo.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { DomainError, Encounter } from "./types.js";
|
||||
|
||||
export interface UndoRedoState {
|
||||
readonly undoStack: readonly Encounter[];
|
||||
readonly redoStack: readonly Encounter[];
|
||||
}
|
||||
|
||||
const MAX_UNDO_STACK = 50;
|
||||
|
||||
export const EMPTY_UNDO_REDO_STATE: UndoRedoState = {
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
};
|
||||
|
||||
export function pushUndo(
|
||||
state: UndoRedoState,
|
||||
snapshot: Encounter,
|
||||
): UndoRedoState {
|
||||
const newStack = [...state.undoStack, snapshot];
|
||||
if (newStack.length > MAX_UNDO_STACK) {
|
||||
newStack.shift();
|
||||
}
|
||||
return { undoStack: newStack, redoStack: [] };
|
||||
}
|
||||
|
||||
export function undo(
|
||||
state: UndoRedoState,
|
||||
currentEncounter: Encounter,
|
||||
): { state: UndoRedoState; encounter: Encounter } | DomainError {
|
||||
if (state.undoStack.length === 0) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "nothing-to-undo",
|
||||
message: "Nothing to undo",
|
||||
};
|
||||
}
|
||||
const restored = state.undoStack.at(-1) as Encounter;
|
||||
return {
|
||||
state: {
|
||||
undoStack: state.undoStack.slice(0, -1),
|
||||
redoStack: [...state.redoStack, currentEncounter],
|
||||
},
|
||||
encounter: restored,
|
||||
};
|
||||
}
|
||||
|
||||
export function redo(
|
||||
state: UndoRedoState,
|
||||
currentEncounter: Encounter,
|
||||
): { state: UndoRedoState; encounter: Encounter } | DomainError {
|
||||
if (state.redoStack.length === 0) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "nothing-to-redo",
|
||||
message: "Nothing to redo",
|
||||
};
|
||||
}
|
||||
const restored = state.redoStack.at(-1) as Encounter;
|
||||
return {
|
||||
state: {
|
||||
undoStack: [...state.undoStack, currentEncounter],
|
||||
redoStack: state.redoStack.slice(0, -1),
|
||||
},
|
||||
encounter: restored,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearHistory(): UndoRedoState {
|
||||
return EMPTY_UNDO_REDO_STATE;
|
||||
}
|
||||
Reference in New Issue
Block a user