# 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 `<Controls>` 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 `<MiniMap>` 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
<ReactFlow
  nodes={nodes}
  edges={edges}
  onNodesChange={onNodesChange}
  nodeTypes={nodeTypes}
  edgeTypes={edgeTypes}
  onNodeClick={handleNodeClick}
  onPaneClick={handlePaneClick}
  fitView
  minZoom={0.25}
  maxZoom={2}
  zoomOnScroll={false}         // plain scroll pans
  zoomOnPinch={true}           // trackpad pinch zooms
  panOnScroll={true}           // plain scroll pans vertically
  panOnScrollMode="vertical"
  selectionOnDrag={false}      // no multi-select
  nodesDraggable={false}       // dagre controls layout
  nodesConnectable={false}     // no edge reconnection
  proOptions={{ hideAttribution: true }}
>
  <Background variant="dots" gap={24} size={1} color="var(--border)" />
  <Controls showInteractive={false} />
  <MiniMap
    pannable
    zoomable
    nodeColor={getNodeColor}
    style={{ display: minimapVisible ? 'block' : 'none' }}
  />
</ReactFlow>
```

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