Files
resolutionflow/docs/plans/archive/2026-02-18-flow-editor-react-flow-design.md
chihlasm 932927b9df chore: archive old plan docs + add survey foundation files
Move completed plan docs to docs/plans/archive/. Add survey migration 046
and reference HTML/plan files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:03:38 -05:00

236 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (~80120px 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)