Files
resolutionflow/docs/plans/2026-02-18-flow-editor-react-flow-design.md
chihlasm 50d9ff59d0 feat: React Flow migration for flow editor canvas (#82)
* docs: add React Flow migration design for flow editor canvas

Replaces hand-built CSS flexbox canvas with @xyflow/react for zoom/pan,
dagre auto-layout, collapsible minimap, and side-panel editing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add React Flow migration implementation plan

12 tasks across 8 phases covering dagre layout, custom nodes,
side panel editor, and full canvas integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: install @xyflow/react and @dagrejs/dagre

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add dagre layout utility for React Flow node positioning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add FlowCanvasNode compact card for React Flow canvas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add FlowCanvasAnswerNode stub card for React Flow canvas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add useTreeLayout hook for tree-to-ReactFlow conversion with dagre

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add NodeEditorPanel side panel for React Flow canvas editing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add FlowCanvas main React Flow component with zoom/pan/minimap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: wire FlowCanvas and NodeEditorPanel into TreeEditorLayout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add panel state management for node editor in TreeEditorPage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: add React Flow dark theme overrides for canvas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: export new React Flow canvas components from barrel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: enable scrolling in node editor panel sidebar

Add min-h-0 to flex containers in the ancestor chain so overflow-y-auto
actually triggers instead of content overflowing off-screen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: constrain tree editor page height to prevent panel overflow

Add overflow-hidden to TreeEditorPage root and NodeEditorPanel container
so the flex height chain is properly constrained by the CSS Grid cell,
preventing the node editor sidebar from growing beyond the viewport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve lint errors in NodeEditorPanel and useTreeLayout

- Fix unused 'children' destructuring with _children prefix
- Move handleClose declaration above the useEffect that references it
- Use handleClose as proper dependency instead of eslint-disable
- Fix unused _parentId parameter type in useTreeLayout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use viewport-based height for node editor panel

Replace h-full with calc(100vh - 105px) to bypass the CSS height chain
that fails to constrain the panel across browsers. The 105px accounts
for the topbar (56px) and editor toolbar (49px).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix canvas controls visibility and enhance dot grid background

- Add !important to all React Flow dark theme overrides to ensure they
  win over library default styles (fixes white controls rectangle)
- Add SVG fill inheritance for control button icons
- Use slightly lighter canvas background (bg-accent/30) so dot grid
  is more visible
- Increase dot size and use muted-foreground color for better contrast

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: collapse sidebar categories with show more/less toggle

Show only the first 4 categories by default with a "N more" button
to expand the full list. Reduces sidebar clutter when many categories
exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:43:34 -05:00

9.5 KiB
Raw Blame History

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

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