Files
resolutionflow/docs/plans/IMPLEMENTATION-PLAN-TREE-EDITOR-CANVAS.md
2026-02-19 01:53:54 -05:00

24 KiB

Implementation Plan: Tree Editor Canvas Redesign

Date: February 17, 2026 Scope: Replace NodeList + NodeEditorModal + TreePreviewPanel in Flow mode with a visual canvas + inline card editing Estimated Components: 3 new files, 4 modified files Phases: 4 (sequential) Branch: feature/tree-editor-canvas


Overview

Replace the current text-outline + modal + passive preview layout with a single-pane visual canvas where nodes are cards, editing is inline, and branches are visually connected. The tree IS the editor — no separate preview panel needed.

Current State

The tree editor uses a text-outline metaphor:

  • Nodes listed as indented rows with ASCII tree lines in NodeList.tsx
  • Editing requires opening NodeEditorModal.tsx for each node (click row → modal opens → edit → Done → modal closes)
  • Passive TreePreviewPanel.tsx takes 40% of space but offers no editing
  • Adding nodes is a two-step picker-then-modal flow
  • Options/branches are opaque — can't see where each branch leads without clicking into the node

Target State (5 Outcomes)

  1. Flow mode uses a full-width TreeCanvas editor; preview panel is removed from Flow mode
  2. Node editing is inline in cards with local draft + ✓ save + ✕ cancel (no modal)
  3. Branches are visually rendered with parent-child connector lines and horizontal splits
  4. Metadata moves to a right slide-in panel, collapsed by default, opened from toolbar in Flow mode only
  5. Code mode remains functionally unchanged (Monaco + preview split)

Layout After Redesign

┌─────────────────────────────────────────────────────────┐
│ TOOLBAR: [Flow Name] [Undo] [Redo] [Metadata] [Save] [Publish] │
├─────────────────────────────────────────────────────────┤
│                                                          │
│   [START]                                               │
│      │                                                   │
│   ┌─────────────────────────────┐                       │
│   │ ? What type of issue?       │ ← Decision card       │
│   │  ↳ [A] Network Issues       │                       │
│   │  ↳ [B] App Errors           │                       │
│   └─────────────────────────────┘                       │
│        │                │                               │
│   [Network card]   [App Errors card]                    │
│        │                                                │
│   [Solution card]                                       │
│                                                          │
│   [+ Add node here]  ← contextual add buttons           │
│                                                          │
└─────────────────────────────────────────────────────────┘

Reusable Code Inventory

Before building anything, confirm these existing pieces are available and understand their APIs:

Asset Location How It's Used
useTreeEditorStore() store/treeEditorStore.ts All CRUD: addNode, updateNode, deleteNode, duplicateNode, reorderNodes, selectNode, findNode, validationErrors
NodeFormDecision components/tree-editor/NodeFormDecision.tsx Decision node fields (question, help_text, options) — will be rendered inline in card
NodeFormAction components/tree-editor/NodeFormAction.tsx Action node fields — will be rendered inline in card
NodeFormResolution components/tree-editor/NodeFormResolution.tsx Solution node fields — will be rendered inline in card
DynamicArrayField components/tree-editor/DynamicArrayField.tsx Reuse for options array in inline card
NodePicker components/tree-editor/NodePicker.tsx Reuse for option target selection (needs allowCreate prop added)
TreeMetadataForm components/tree-editor/TreeMetadataForm.tsx Wraps into MetadataSidePanel as-is
cn() @/lib/utils Tailwind class merging utility
Design tokens tailwind.config.js bg-card, border-border, text-foreground, font-heading, font-label
Brand colors tailwind.config.js Blue (decision), Yellow (action), Green (solution)
buildSharedLinksMap() components/tree-preview/TreePreviewPanel.tsx Shared node detection logic — extract and reuse for jump/reference indicators

IMPORTANT: These rules govern how the canvas renders node connections. Read before implementing Phase 2.

Tree-First Rendering

Render only structural children (nodes in node.children[]) with visual connector lines. Do NOT draw cross-canvas connector lines for shared/cross-linked nodes.

For links to non-child/shared targets (where option.next_node_id points to a node that is NOT a direct child), show a compact "jump/reference" indicator badge in the card content instead of a connector line.

Decision Child Lane Ordering

When a decision node has children, order the horizontal child lanes:

  1. First: Children whose id matches an option.next_node_id — ordered by option order
  2. Then: Append any remaining unlinked children

Action Next-Child Rule

  • If a child's id matches the action node's next_node_id, treat it as the primary "next" lane
  • If no child matches next_node_id, keep child visible but show the link as an "unbound reference" indicator

Deletion Safety

Before calling deleteNode(nodeId), the canvas must clean up all inbound references:

  • Scan all decision nodes: clear any options[].next_node_id that equals the deleted node's ID
  • Scan all action nodes: clear any next_node_id that equals the deleted node's ID

This prevents stale link references. The current treeEditorStore.deleteNode() does NOT do this cleanup — the canvas orchestration layer handles it.


Phase 1: TreeCanvasNode Component (Core Inline Editor Card)

File: frontend/src/components/tree-editor/TreeCanvasNode.tsx (NEW)

Props Interface

interface TreeCanvasNodeProps {
  node: TreeStructure
  depth: number
  fromOption?: string           // Which parent option label leads here
  isExpanded: boolean
  isNew: boolean                // Show "Unsaved" badge, cancel triggers delete
  onToggleExpand: () => void
  onSave: (nodeId: string, updates: Partial<TreeStructure>) => void
  onCancelNew: (nodeId: string) => void  // Delete unsaved node
  onDelete: (nodeId: string) => void
  onDuplicate: (nodeId: string) => void
  onDragStart: (e: React.DragEvent, nodeId: string) => void
  onDragOver: (e: React.DragEvent) => void
  onDrop: (e: React.DragEvent) => void
}

Card States

Compact (default):

  • Node type badge/icon (Decision ?, Action , Solution ✓)
  • Title text (question for decisions, title for action/solution)
  • Option labels or option count for decisions
  • Validation error badge (red dot if errors on this node)
  • "Unsaved" badge (yellow, if isNew)
  • Click anywhere on card → calls onToggleExpand

Expanded (editing):

  • Local draft state: const [draft, setDraft] = useState<Partial<TreeStructure>>(() => cloneNodeWithoutChildren(node))
  • Renders the appropriate existing form subcomponent inline:
    • Decision: <NodeFormDecision node={draft} onUpdate={setDraftField} />
    • Action: <NodeFormAction node={draft} onUpdate={setDraftField} />
    • Solution: <NodeFormResolution node={draft} onUpdate={setDraftField} />
  • Header actions row:
    • ✓ Save button → calls onSave(node.id, draft) (strip children from draft before passing)
    • ✕ Cancel button → if isNew, calls onCancelNew(node.id). Otherwise resets draft to node values and collapses
    • Duplicate button (hide if root)
    • Delete button (hide if root)
  • Drag handle in header (hide if root)

Card Styling

Aesthetic direction: "Precision engineering tool" — clean, minimal chrome, confident typography.

All cards:       bg-card border-border rounded-xl shadow-sm
Decision:        border-l-4 border-blue-500
Action:          border-l-4 border-yellow-500
Solution:        border-l-4 border-green-500
Expanded ring:   ring-1 ring-primary
Titles:          font-heading (Plus Jakarta Sans)
Type badges:     font-label (Outfit)

Implementation Steps

  1. Create TreeCanvasNode.tsx with compact view only (type badge, title, option count)
  2. Add expanded view with local draft state and inline form rendering
  3. Wire save/cancel/delete/duplicate actions
  4. Add drag handle events
  5. Add validation badge and unsaved badge
  6. Style with brand tokens

Verification

  • Render a single card in isolation with mock data
  • Confirm compact → expanded toggle works
  • Confirm save commits draft (log output), cancel resets
  • Confirm cancel on isNew=true calls onCancelNew
  • Run npm run build — no TypeScript errors

Phase 2: TreeCanvas Component (Layout & Orchestration)

File: frontend/src/components/tree-editor/TreeCanvas.tsx (NEW)

Canvas State Model

// Local UI state (NOT in Zustand store — canvas-only concerns)
const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(new Set())
const [newNodeIds, setNewNodeIds] = useState<Set<string>>(new Set())
const [pendingAddTarget, setPendingAddTarget] = useState<string | null>(null)
const [pendingLinkByNodeId, setPendingLinkByNodeId] = useState<Map<string, {
  parentId: string
  optionId?: string  // For decision option linking
}>>(new Map())
const [dragState, setDragState] = useState<{
  nodeId: string
  parentId: string
  index: number
} | null>(null)

Single expanded card policy: Only one card expanded at a time. When expanding a card, collapse the previously expanded one.

Rendering

Recursive rendering of treeStructure from the store:

  • Root at top, rendered as a "START" card
  • Vertical flow downward
  • When a decision node has multiple options with children, render children in horizontal lanes side-by-side
  • Single-pane scrollable area (overflow-auto)
  • Background: bg-background with subtle CSS radial dot grid pattern

Connector Lines

Use CSS borders (not SVG) for connecting lines:

  • Parent-to-children trunk line: border-l border-border extending down from parent
  • Horizontal fork line: border-t border-border connecting sibling lane tops
  • Vertical stubs: border-l border-border dropping into each child card

Add-Node Flow

Parent Type Add Button Behavior
Decision Show + Add child per option row (next to each option label)
Action Show single + Add child below the card
Solution No add button (terminal node)
  • + Add buttons use dashed border, appear on hover of parent card bottom edge
  • Clicking + Add sets pendingAddTarget → shows inline type picker (decision/action/solution buttons) at that position
  • Selecting a type:
    1. Calls addNode(parentId, type) → gets new node ID
    2. Adds node ID to newNodeIds
    3. Adds entry to pendingLinkByNodeId (parent ID + option ID if from a decision option)
    4. Auto-expands the new node card
    5. Clears pendingAddTarget

Save Behavior for New Child Nodes

When user clicks ✓ on a new node:

  1. Call updateNode(nodeId, draft) to save content to store
  2. If pendingLinkByNodeId.has(nodeId):
    • Get the pending link info (parentId, optionId)
    • If optionId exists: update parent's options[].next_node_id to point to this node
    • If no optionId (action parent): update parent's next_node_id to point to this node
  3. Remove from newNodeIds
  4. Remove from pendingLinkByNodeId

Cancel Behavior for New Nodes

When user clicks ✕ on a new (unsaved) node:

  1. Call deleteNode(nodeId)
  2. Remove from newNodeIds
  3. Remove from pendingLinkByNodeId

Delete Behavior (Any Node)

When user clicks delete on any node:

  1. Clean inbound references first (see Link Model section above):
    • Walk the full tree and clear any options[].next_node_id or next_node_id matching the node being deleted
  2. Then call deleteNode(nodeId)
  3. Remove from expandedNodeIds if present

Sibling Reorder

  • Drag handle in card header (Phase 1 wired the events)
  • Drop zones rendered between sibling cards (visual indicator line)
  • On drop: call reorderNodes(parentId, fromIndex, toIndex)

Selection Integration

  • Click on a card calls selectNode(nodeId) in the store
  • Watch selectedNodeId from store (including changes from ValidationSummary clicks):
    • Auto-expand the selected node's card
    • Scroll the card into view with scrollIntoView({ behavior: 'smooth', block: 'nearest' })

Implementation Steps

  1. Create TreeCanvas.tsx with recursive tree rendering (compact cards only, no editing)
  2. Add CSS connector lines between parent and children
  3. Add horizontal branching for decision nodes with multiple children
  4. Add canvas state model (expandedNodeIds, newNodeIds, etc.)
  5. Wire card expand/collapse with single-expanded-card policy
  6. Add inline type picker and add-node flow with pending link tracking
  7. Wire save/cancel with pending link resolution
  8. Wire delete with inbound reference cleanup
  9. Add drag-and-drop sibling reorder
  10. Add selection sync (auto-expand + scroll into view)
  11. Add grid background pattern
  12. Style add buttons (dashed border, hover reveal)

Verification

  • Create a new tree → see canvas with root START card
  • Click root → expands inline (no modal)
  • Add 2 options, save → see branch lanes
  • Add child from each option → pending link resolves on save
  • Cancel a new node → node deleted, link cleaned up
  • Delete a linked node → parent's reference cleared
  • Drag reorder siblings
  • Run npm run build — no TypeScript errors

Phase 3: Form Refactoring + Layout Update

3A: Form Refactoring for Inline Reuse Safety

Files to modify:

  • frontend/src/components/tree-editor/NodeFormDecision.tsx
  • frontend/src/components/tree-editor/NodePicker.tsx

NodeFormDecision.tsx Changes

Problem: Currently, option reordering calls reorderOptions() directly on the store. In inline canvas editing, this would write to the store before the user clicks ✓ save (breaking the local draft model).

Fix: Change option reordering to mutate the local node.options array through the onUpdate callback instead of calling the store directly.

// BEFORE (writes to store immediately):
const handleReorderOptions = (fromIndex: number, toIndex: number) => {
  reorderOptions(node.id, fromIndex, toIndex)
}

// AFTER (mutates local draft via onUpdate):
const handleReorderOptions = (fromIndex: number, toIndex: number) => {
  const newOptions = [...(node.options || [])]
  const [moved] = newOptions.splice(fromIndex, 1)
  newOptions.splice(toIndex, 0, moved)
  onUpdate({ options: newOptions })
}

Keep modal compatibility: This change is backward-compatible. In the legacy modal path, onUpdate already propagates to the store. In the canvas path, onUpdate updates the local draft.

NodePicker.tsx Changes

Problem: NodePicker currently has create-new-node options (__create_decision__, etc.) that call addNode() on the store. In canvas inline editing, this would create nodes as a side effect of browsing the picker during draft editing.

Fix: Add an allowCreate prop:

interface NodePickerProps {
  // ... existing props
  allowCreate?: boolean  // default: true
}
  • When allowCreate={false}, hide the "Create New" option group
  • Pass allowCreate={false} from TreeCanvasNode expanded editing
  • Pass allowCreate={true} (default) in legacy modal path

3B: Layout Update

File: frontend/src/components/tree-editor/TreeEditorLayout.tsx

Changes

interface TreeEditorLayoutProps {
  isMobile?: boolean
  isMetadataOpen: boolean        // NEW
  onCloseMetadata: () => void    // NEW
}

Flow mode (replace the 60/40 split):

BEFORE:
  <div className="w-3/5">
    <TreeMetadataForm />
    <NodeList />
  </div>
  <div className="flex-1">
    <TreePreviewPanel />
  </div>

AFTER:
  <TreeCanvas />  (full width)
  <MetadataSidePanel isOpen={isMetadataOpen} onClose={onCloseMetadata} />  (overlay)

Code mode: Unchanged. Keep existing 60/40 Monaco + preview behavior.

3C: MetadataSidePanel Component

File: frontend/src/components/tree-editor/MetadataSidePanel.tsx (NEW)

Right-side slide-in panel (320px wide) that wraps <TreeMetadataForm />.

interface MetadataSidePanelProps {
  isOpen: boolean
  onClose: () => void
}

Behavior:

  • Slides in from right edge, overlays the canvas (does NOT resize it)
  • Close triggers: panel close button, backdrop click, Escape key
  • Uses existing overlay/backdrop pattern from other modals in the app
  • Only rendered in Flow mode

Implementation Steps

  1. Refactor NodeFormDecision.tsx — option reorder through onUpdate
  2. Add allowCreate prop to NodePicker.tsx
  3. Create MetadataSidePanel.tsx
  4. Update TreeEditorLayout.tsx — swap Flow mode layout, add metadata panel props
  5. Verify legacy modal path still works (if still referenced anywhere)

Verification

  • Open tree editor in Flow mode → see full-width canvas (no 60/40 split)
  • Open tree editor in Code mode → see unchanged Monaco + preview layout
  • Click Metadata button → panel slides in from right, canvas doesn't resize
  • Close metadata panel via close button, backdrop click, and Escape key
  • Edit options in inline card → reorder does NOT write to store until ✓ save
  • NodePicker in inline card → no "Create New" options shown
  • Run npm run build — no TypeScript errors

Phase 4: Toolbar Wiring + Exports

File: frontend/src/pages/TreeEditorPage.tsx (modify)

Changes

  1. Add local state: const [isMetadataOpen, setIsMetadataOpen] = useState(false)
  2. Add "Metadata" toolbar button — visible in Flow mode only
  3. Auto-close metadata panel when switching to Code mode:
    // In mode switch handler:
    if (newMode === 'code') setIsMetadataOpen(false)
    
  4. Pass metadata props into TreeEditorLayout:
    <TreeEditorLayout
      isMetadataOpen={isMetadataOpen}
      onCloseMetadata={() => setIsMetadataOpen(false)}
    />
    
  5. Keep all existing toolbar actions unchanged: undo/redo, save/publish, validate, analytics

File: frontend/src/components/tree-editor/index.ts (modify)

Add exports:

export { TreeCanvas } from './TreeCanvas'
export { TreeCanvasNode } from './TreeCanvasNode'
export { MetadataSidePanel } from './MetadataSidePanel'

Keep all legacy exports (NodeList, NodeEditorModal, TreePreviewPanel) — they remain in the codebase but are no longer imported in the active Flow path.

Implementation Steps

  1. Add metadata panel state and toolbar button to TreeEditorPage.tsx
  2. Add auto-close on Code mode switch
  3. Pass props through to TreeEditorLayout
  4. Update index.ts exports
  5. Verify no dead imports remain

Verification

  • Flow mode toolbar shows "Metadata" button
  • Code mode toolbar does NOT show "Metadata" button
  • Click Metadata → panel opens. Switch to Code → panel auto-closes
  • Undo/redo/save/publish/validate all still work
  • Run npm run build — no TypeScript errors

Critical Files Summary

File Action Phase Notes
TreeCanvasNode.tsx Create 1 Inline-editing card with local draft + commit model
TreeCanvas.tsx Create 2 Main canvas orchestration with full state model
MetadataSidePanel.tsx Create 3 320px right slide-in overlay wrapping TreeMetadataForm
NodeFormDecision.tsx Refactor 3 Option reorder through onUpdate (remove store write)
NodePicker.tsx Refactor 3 Add allowCreate prop (default true)
TreeEditorLayout.tsx Modify 3 Replace 60/40 split with full-width canvas + overlay
TreeEditorPage.tsx Modify 4 Add metadata panel toggle, auto-close on Code switch
index.ts Update 4 Export new components, keep legacy exports
treeEditorStore.ts No changes Store logic is solid as-is
NodeList.tsx Keep (legacy) Removed from active Flow path
NodeEditorModal.tsx Keep (legacy) Removed from active Flow path
TreePreviewPanel.tsx Keep (legacy) Removed from active Flow path

Assumptions & Defaults

  • Link rendering: Tree-first (no full cross-canvas graph lines for shared targets)
  • Inline edit commit: On checkmark save (local draft, cancel discards)
  • Metadata drawer: Flow mode only
  • Expanded card policy: One expanded card at a time
  • Legacy components: Remain in repo, removed from active Flow path only
  • Connector lines: CSS borders (not SVG — simpler, matches existing patterns)

Testing

Automated Tests

Files: TreeCanvas.test.tsx, TreeCanvasNode.test.tsx

Test Case What It Verifies
Select node expands and scrolls card Selection sync works
Inline save commits store updates Draft → store pipeline works
Cancel on new node triggers deletion Unsaved node cleanup works
Add child from decision option sets next_node_id on save Pending link resolution works
Delete node clears inbound references Reference cleanup works
Sibling drag reorder calls reorderNodes Drag-and-drop wiring works
allowCreate={false} hides create options in NodePicker Form safety works
Option reorder in inline card does NOT write to store Draft isolation works

Run: npm run build && npm run test (or targeted vitest files)

Manual Acceptance Checklist

  • Create a new tree — canvas shows root START card
  • Click root card — expands inline with decision fields (no modal appears)
  • Fill in question, add 2 options, click ✓ — saves inline, see branch lanes
  • + Add child buttons appear below each option
  • Add a child node inline — verify parent link is set correctly
  • Cancel a new unsaved node — confirm it is deleted from the tree
  • Delete a node that is referenced by another — confirm references are cleaned
  • Drag to reorder sibling nodes
  • Open Metadata panel — edit metadata — close panel (button, backdrop, Escape)
  • Validate and Publish — confirm tree saves correctly
  • Switch Flow → Code mode — metadata panel auto-closes, Code mode works normally
  • Switch Code → Flow mode — canvas renders correctly
  • Run npm run build — no TypeScript errors
  • Run npm run test — all tests pass

Git Strategy

  • Branch: feature/tree-editor-canvas
  • One commit per phase (4 commits total)
  • Commit messages:
    1. feat: Add TreeCanvasNode inline editor card component
    2. feat: Add TreeCanvas layout with visual branching and orchestration
    3. refactor: Update forms for inline safety, add MetadataSidePanel, update layout
    4. feat: Wire toolbar metadata toggle and update exports
  • PR when all 4 phases pass npm run build && npm run test
  • Include Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Notes for Implementation

  1. Read existing code first: Before creating any new file, read the files listed in the Reusable Code Inventory to understand current patterns and prop interfaces
  2. Follow existing patterns: Match the component structure, Tailwind usage, and TypeScript conventions already in the tree-editor directory
  3. Dark mode: All new components must support light/dark themes via existing Tailwind classes
  4. Keyboard navigation: Support Escape to close metadata panel, Tab through form fields in expanded cards
  5. Loading states: Canvas should handle the case where treeStructure is null (show empty state)
  6. No store changes: The treeEditorStore.ts should NOT be modified. All new state is local to the canvas components
  7. Test each phase independently: Each phase should leave the app in a buildable, testable state before moving to the next