PURPOSE
Command-pattern undo/redo stack for the Playground designer screen. Each parameter change (slider, dropdown, toggle) becomes a HistoryCommand with execute() and undo(). Provides multi-level undo/redo capped at 200 commands, a timestamped change log for designer review, and a session-start snapshot for full reset. Slider drags are debounced upstream: commands are created on pointerUp, not on every onChange tick.
OWNS
HistoryCommandinterface —key,label,prev,next,timestamp,execute(),undo().HistoryStackinterface —commands[],pointer,sessionSnapshot.MAX_HISTORYconstant capping the stack at 200 commands.- Stack factory
createHistoryStack()and snapshot settersaveSessionSnapshot(). - Stack operations
pushCommand(),undo(),redo(),canUndo(),canRedo(),clearHistory(). - Log readers
getLog()(commands up to pointer) andgetFullLog()(all commands, each tagged withisUndone). - Command factory
createValueCommand()that mutates a state object at a dotted key path (e.g.palette.base). - Display formatters
formatValue()(numbers trim trailing zeros, booleans renderON/OFF, strings pass through, other types stringify as JSON) andformatTime()(HH:MM:SS).
READS FROM
Date.now()for command timestamps increateValueCommand().structuredClone()for the session-start snapshot deep copy.- The caller-supplied
stateobject insidecreateValueCommand— walked by dotted key path onexecute/undo.
PUSHES TO
- The caller-supplied
stateobject —execute()writesnext,undo()restoresprevvia mutation at the resolved key path. - The
HistoryStackpassed topushCommand()/undo()/redo()/clearHistory()— mutatescommandsandpointerin place; trims the redo tail when a new command is pushed past a partial undo; shifts the oldest command and decrements the pointer whenMAX_HISTORYis reached. stack.sessionSnapshot—saveSessionSnapshot()deep-clones the provided state into the field.
DOES NOT
- Does not perform the parameter mutation itself outside the supplied
execute/undoclosures — callers build the command and the file just runs it. - Does not debounce slider input — debouncing is the caller’s responsibility (commands are expected to arrive on pointerUp).
- Does not restore from
sessionSnapshot— the snapshot is stored but applying it is the caller’s job. - Does not bind to keyboard shortcuts, render any UI, or coalesce adjacent commands with the same key.
- Does not validate that the dotted key path in
createValueCommandexists on the state object — missing intermediate nodes will throw at mutation time. - Does not emit events or notify listeners — consumers re-read stack state after each operation.
- Does not persist the stack across reloads.
Signals
pushCommand()truncatescommandstopointer + 1before appending, discarding any redo tail.- When the stack reaches
MAX_HISTORY,pushCommand()drops the oldest entry viashift()and decrementspointerso the live position stays aligned. undo()returnsfalsewhenpointer < 0;redo()returnsfalsewhen at the head of the stack — both are safe to call repeatedly.getFullLog()returns the entire stack with anisUndoneflag (i > pointer) for grayed-out display of redoable commands.formatValue()trims trailing zeros from numbers with the regex/\.?0+$/aftertoFixed(3), so1.500renders as1.5and2.000renders as2.
Entry points
- Playground screen modules that wire sliders/toggles to parameter state instantiate one
HistoryStackviacreateHistoryStack(), callsaveSessionSnapshot()on mount, and route every committed change throughcreateValueCommand()+pushCommand(). - Undo/redo UI buttons call
undo(stack)/redo(stack)and gate enablement oncanUndo(stack)/canRedo(stack). - Change-log panels render
getFullLog(stack)usingformatValue()andformatTime()for display. - “Reset to Session Start” actions read
stack.sessionSnapshot(snapshot apply lives in caller code).
Pattern notes
- Pure command-pattern implementation — the file holds no module-level state; each
HistoryStackis an owned plain object the caller passes around explicitly. - All operations are mutating functions on a passed-in stack, not methods on a class — consistent with the project’s data-table-plus-functions style.
- Commands store
prev/nextasunknown, so the stack is type-agnostic: numbers, strings, booleans, and nested objects all flow through the same path. createValueCommand()builds the mutation closures once at construction time, capturingpartsandstate—execute/undothen walk the path on each invocation rather than caching the parent node, so reassignment of intermediate objects between operations is tolerated.- Redo tail truncation uses
commands.length = stack.pointer + 1rather thansplice, avoiding the intermediate array allocation. - The 200-command cap is a hard ceiling rather than a soft hint — overflow is silently dropped from the head.