## Context
The Planner uses Yjs for collaborative CRDT state with four shared types:
`waypoints` (Y.Array), `noGoAreas` (Y.Array), `routeData` (Y.Map), and
`notes` (Y.Text). All mutations happen through Yjs transactions. The editor
supports multiple simultaneous users via y-websocket.
Yjs provides a built-in `UndoManager` class that tracks changes to specified
shared types, scoped by transaction origin, with configurable capture
timeouts for grouping related changes.
## Goals / Non-Goals
**Goals:**
- Undo/redo for waypoint operations (add, delete, reorder, drag-move)
- Undo/redo for no-go area operations (add, delete)
- Undo/redo for notes text edits
- Keyboard shortcuts that work naturally (Ctrl+Z, Ctrl+Shift+Z, Ctrl+Y)
- Topbar buttons with visual disabled state
- Collaborative-safe: only undo your own changes, never other users'
- Drag operations grouped as a single undo step
**Non-Goals:**
- Undo for routeData (derived data, not user input)
- Undo for route options / color mode changes
- Persistent undo history across page reloads
- Customizable undo stack depth
## Decisions
### D1: Single UndoManager tracking waypoints, noGoAreas, and notes
Create one `Y.UndoManager` instance in `use-yjs.ts`, tracking all three
user-editable shared types: `waypoints`, `noGoAreas`, and `notes`.
```ts
const undoManager = new Y.UndoManager(
[waypoints, noGoAreas, notes],
{
captureTimeout: 500,
trackedOrigins: new Set(["local"]),
}
);
```
A single UndoManager means undo walks back through all user actions in order,
regardless of which shared type was modified. This matches user expectations:
"undo my last thing" not "undo my last waypoint thing."
The UndoManager is created inside the `useYjs` hook and exposed on the
`YjsState` interface so all components can access it.
### D2: Transaction origins for local vs. remote scoping
All local mutations must use the origin `"local"` so UndoManager can
distinguish them from remote changes arriving via y-websocket:
```ts
doc.transact(() => {
waypoints.push([yMap]);
}, "local");
```
Current code has some `doc.transact()` calls without an origin and some
direct mutations (e.g., `yjs.waypoints.push([yMap])`) outside a transaction.
All mutation sites need to be wrapped in `doc.transact(() => { ... }, "local")`.
Mutation sites to update:
- `PlannerMap.tsx`: `addWaypoint`, `insertWaypointAtSegment`, `moveWaypoint`,
`deleteWaypoint`
- `WaypointSidebar.tsx`: `deleteWaypoint`, `moveWaypoint`
- `NoGoAreaLayer.tsx`: no-go area creation (`pm:create` handler), no-go area
deletion (contextmenu handler)
- `NotesPanel.tsx`: `handleInput` (already uses `doc.transact`, needs origin)
- `use-yjs.ts`: initial waypoint seeding (use `"init"` origin, not `"local"`,
so it's not undoable)
### D3: Keyboard shortcuts
Register a global `keydown` listener (in a `useUndoShortcuts` hook or
similar) that handles:
- **Ctrl+Z** (or Cmd+Z on macOS): `undoManager.undo()`
- **Ctrl+Shift+Z** or **Ctrl+Y** (Cmd+Shift+Z on macOS): `undoManager.redo()`
The listener must suppress when the active element is an ``,
`