# 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)