diff --git a/docs/plans/2026-02-18-flow-editor-react-flow-design.md b/docs/plans/2026-02-18-flow-editor-react-flow-design.md new file mode 100644 index 00000000..efa3df29 --- /dev/null +++ b/docs/plans/2026-02-18-flow-editor-react-flow-design.md @@ -0,0 +1,235 @@ +# Flow Editor — React Flow Migration Design + +> **Date:** 2026-02-18 +> **Scope:** Replace hand-built CSS flexbox canvas with @xyflow/react for zoom, pan, auto-layout, and improved collapse UX + +## Overview + +The current flow editor canvas (`TreeCanvas.tsx`) uses pure CSS flexbox to position nodes. This works for small trees but breaks down with large flows — nodes overlap, there's no zoom/pan, and collapsing subtrees is hard to discover. This design replaces the canvas with React Flow (`@xyflow/react`), adds dagre-based auto-layout, and moves editing to a right-side panel. + +## Problems Solved + +1. **No zoom/pan** — users can only scroll; can't zoom out for a bird's-eye view or zoom into a section +2. **Node overlap** — wide trees with many branches cause flexbox lanes to overlap +3. **Collapse is hidden** — the subtree collapse toggle is a small icon in the node header, easy to miss +4. **Inline editing bloats cards** — expanded cards are huge, disrupting the visual tree layout + +## Architecture + +### Source of Truth + +The Zustand store's `treeStructure` (recursive nested object) remains the single source of truth. No store changes required. The canvas maintains a **derived** flat representation (`Node[]` and `Edge[]`) computed from the tree structure. + +### Data Flow + +``` +treeStructure (Zustand) → useTreeLayout hook → { nodes, edges } → ReactFlow → renders + ↓ + user clicks node → NodeEditorPanel opens + user saves edits → updateNode(id, data) → store updates → re-derive +``` + +### New Dependencies + +- `@xyflow/react` — canvas framework (MIT, 20k+ GitHub stars) +- `@dagrejs/dagre` — directed graph layout algorithm +- `@types/dagre` — TypeScript types + +## Interactions + +### Zoom + +Ctrl/Cmd + scroll wheel to zoom. Zoom range: 25%–200%. Plain scroll pans vertically (natural page scrolling feel). + +### Pan + +Click and drag on empty canvas space. Plain scroll pans vertically. Middle-click drag also pans. + +### Node Selection + +Single-click on a node body selects it and opens the side panel editor. + +### Subtree Collapse + +Single-click on a visible chevron icon at the bottom edge of any node that has children. Always visible (not behind hover). When collapsed: +- Children and their edges are removed from the React Flow graph entirely +- A pill below the node shows "N nodes hidden" +- Clicking the pill or chevron again expands + +### Zoom Controls + +Small floating toolbar in bottom-left corner: zoom in (+), zoom out (−), fit-to-view. Uses React Flow's built-in `` component. + +### Minimap + +Bottom-right corner. Collapsible via a toggle button — user can minimize or close it. Pannable and zoomable (click on minimap to jump to that area). Uses React Flow's built-in `` component. Node colors in minimap match type accent colors (blue/yellow/green). + +### Fit View + +Auto-fits on initial load and when clicking the fit button. Applies padding so nodes aren't pressed against viewport edges. + +## Custom Node Types + +Four React Flow custom node types, all **compact** (no inline editing): + +| Type | Accent | Icon | Content | +|------|--------|------|---------| +| `decision` | Blue left border | `HelpCircle` | Question text (1-2 lines), "N options" badge, option labels | +| `action` | Yellow left border | `Zap` | Title, description preview (truncated) | +| `solution` | Green left border | `CheckCircle` | Title, description preview (truncated) | +| `answer` | Dashed border, muted | — | Label + "Choose Type" prompt | + +### Card Specs + +- **Width:** 280px (fixed — gives dagre consistent widths) +- **Height:** Variable based on content (~80–120px estimated) +- **Selected state:** `ring-1 ring-primary` +- **Validation errors:** Red dot badge on nodes with errors +- **Collapse chevron:** Visible at bottom of node when it has children + +### Edges + +Smoothstep edges (React Flow built-in) — route around nodes with rounded corners. Color: `border-border`. + +**Edge labels:** Show the option text leading to each child. **Truncated to 35 characters + ellipsis** for long option text (e.g., "User reports intermittent VPN di…"). Full text visible on hover tooltip and in the side panel when the parent decision is selected. + +## Side Panel Editor + +When a node is selected, a right-side editor panel opens. The canvas container **resizes** (shrinks by panel width) rather than the panel overlaying the canvas — this prevents covering the selected node. React Flow handles container resize natively. + +### Panel Specs + +- **Width:** 400px +- **Position:** Right side, part of the layout (not floating) +- **Open triggers:** Single-click a node +- **Close triggers:** X button, Escape key, clicking empty canvas +- **Auto-center:** When panel opens, auto-pan so the selected node stays centered in the remaining canvas area (via React Flow's `setCenter` / `fitBounds`) + +### Panel Structure + +- **Header:** Node type icon + badge, node title (or "New Decision"), close button +- **Body:** Renders existing form components — `NodeFormDecision`, `NodeFormAction`, `NodeFormResolution`. For `answer` nodes: type picker buttons (Decision/Action/Solution) +- **Footer:** Save (`bg-gradient-brand`), Cancel, Delete (with confirmation), Duplicate + +### Draft Model + +Same local-draft-then-commit pattern as current inline editor: +- Panel opens with a clone of the node data +- Edits modify the draft only +- Save writes to Zustand store → triggers re-derive of React Flow nodes/edges +- Cancel discards draft and closes panel +- Switching nodes while editing prompts save/discard + +### Panel Coexistence + +Only one right panel open at a time. Opening node editor closes metadata panel and vice versa. + +## Layout Engine (Dagre) + +### Configuration + +- Direction: `rankdir: 'TB'` (top-to-bottom) +- Node width: 280px +- Node height: estimated heuristic (~80px base + content) +- Rank separation (vertical gap): ~100px +- Node separation (horizontal gap): ~40px + +### Height Measurement Correction + +Dagre needs node heights before rendering, but content varies. Strategy: +1. **First pass:** Use heuristic height estimates based on node type and content length +2. **After first paint:** Measure actual rendered heights via refs +3. **If any height differs by >10px from estimate:** Re-run dagre with actual heights (single correction pass, no infinite loops) + +This avoids visible layout jumps in most cases while catching edge cases like decision nodes with 8+ options. + +### When Re-layout Runs + +| Trigger | Re-layout? | +|---------|-----------| +| Node added/deleted | Yes | +| Node moved (reparented) | Yes | +| Options added/removed on a decision (structural change) | Yes | +| Content-only edits (title, description text) | No | +| Collapse/expand toggle | Yes (different nodes visible) | +| Panel open/close | No (React Flow handles container resize) | + +After re-layout, `fitView` is called with padding. + +## File Changes + +### New Files + +| File | Purpose | +|------|---------| +| `components/tree-editor/FlowCanvas.tsx` | React Flow canvas — replaces `TreeCanvas.tsx` | +| `components/tree-editor/FlowCanvasNode.tsx` | Custom compact node component (decision/action/solution) | +| `components/tree-editor/FlowCanvasAnswerNode.tsx` | Custom node for answer stubs | +| `components/tree-editor/NodeEditorPanel.tsx` | Right-side editor panel — replaces inline card editing | +| `components/tree-editor/useTreeLayout.ts` | Hook: treeStructure → nodes/edges + dagre + measure-correct | +| `lib/dagreLayout.ts` | Pure function: positioned nodes from dagre | + +### Modified Files + +| File | Changes | +|------|---------| +| `TreeEditorLayout.tsx` | Flow mode renders `FlowCanvas` + `NodeEditorPanel` instead of `TreeCanvas` | +| `TreeEditorPage.tsx` | Panel state: node editor vs metadata, single-panel-at-a-time | + +### Unchanged + +- `treeEditorStore.ts` — no store changes needed +- `NodeFormDecision.tsx`, `NodeFormAction.tsx`, `NodeFormResolution.tsx` — reused inside panel +- `MetadataSidePanel.tsx` — already works, gets single-panel-at-a-time rule +- Code mode — completely untouched + +### Removed from Active Flow Mode Path + +- `TreeCanvas.tsx` → replaced by `FlowCanvas.tsx` +- `TreeCanvasNode.tsx` → replaced by `FlowCanvasNode.tsx` +- `AnswerStubCard.tsx` → logic moves to `FlowCanvasAnswerNode.tsx` + +Old components stay in the repo but are no longer imported in Flow mode. + +## React Flow Configuration + +```tsx + + + + + +``` + +## Not Included (YAGNI) + +- No drag-to-reparent nodes on canvas +- No visual edge reconnection (dragging edges) +- No multi-select nodes +- No undo/redo on canvas position changes (undo/redo stays on tree data only) +- No manual node drag repositioning (dagre controls layout) +- No light mode (dark-first design system)