zudo-panel-designer docs
GitHub repository

Type to search...

to open search from anywhere

Undo / redo history

The snapshot-based history reducer, gesture batching, and the 50-entry cap, from history.ts.

A snapshot-based reducer

Undo/redo is a plain, React-free reducer over full-state snapshots of the document — not a diff/patch log:

interface HistoryState<T> {
  past: T[];
  present: T;
  future: T[];
}

createHistory(initial) starts with an empty past/future and present = initial. Because the reducer is plain functions with no React dependency, @zpd/core can be tested and used standalone; the app wave wires these into whatever state container it uses.

commit vs. replace

FunctionEffect
commit(state, next)Pushes the current present onto past as a new undo entry, sets next as the new present, and discards any redo branch (future is cleared).
replace(state, next)Updates present in place — no new undo entry is created.

replace exists for coalescing continuous, in-flight updates — e.g. every pointermove of a drag — into whichever entry the most recent commit/beginGesture opened, instead of creating an undo step per pixel of movement.

Gesture batching

function beginGesture<T>(state: HistoryState<T>): HistoryState<T> {
  // Same as committing the current present onto itself
  return commit(state, state.present);
}

beginGesture opens a new undo entry without changing present yet. The calling pattern is: call beginGesture once when a drag/resize/node-edit interaction starts, then call replace on every intermediate update for the rest of that gesture. The net effect is that one entire gesture — however many pointer events it produces — collapses into exactly one undo entry.

The 50-entry cap

const MAX_HISTORY = 50;

commit (and therefore beginGesture, which is implemented via commit) slices past to its last 50 entries on every push: past: [...state.past, state.present].slice(-MAX_HISTORY). History depth is bounded — undo does not grow without limit over a long editing session.

Undo, redo, and the can-* guards

FunctionEffect
undo(state)Moves the last past entry into present, and pushes the previous present onto the front of future. No-op if past is empty.
redo(state)Moves the first future entry into present, and pushes the previous present onto past. No-op if future is empty.
canUndo(state)true when past.length > 0.
canRedo(state)true when future.length > 0.

Because commit always clears future, redo is only available immediately after an undo — any new edit after an undo discards the abandoned redo branch, which is standard undo/redo semantics.