* 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>
155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
import { memo } from 'react'
|
|
import { Handle, Position, type NodeProps } from '@xyflow/react'
|
|
import { HelpCircle, Zap, CheckCircle, ChevronDown, ChevronRight, AlertCircle } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
import type { TreeStructure, NodeType } from '@/types'
|
|
|
|
const NODE_TYPE_CONFIG: Record<Exclude<NodeType, 'answer'>, {
|
|
icon: typeof HelpCircle
|
|
label: string
|
|
borderClass: string
|
|
badgeClass: string
|
|
minimapColor: string
|
|
}> = {
|
|
decision: {
|
|
icon: HelpCircle,
|
|
label: 'Decision',
|
|
borderClass: 'border-l-4 border-l-blue-500',
|
|
badgeClass: 'bg-blue-500/20 text-blue-400',
|
|
minimapColor: '#3b82f6',
|
|
},
|
|
action: {
|
|
icon: Zap,
|
|
label: 'Action',
|
|
borderClass: 'border-l-4 border-l-yellow-500',
|
|
badgeClass: 'bg-yellow-500/20 text-yellow-400',
|
|
minimapColor: '#eab308',
|
|
},
|
|
solution: {
|
|
icon: CheckCircle,
|
|
label: 'Solution',
|
|
borderClass: 'border-l-4 border-l-green-500',
|
|
badgeClass: 'bg-green-500/20 text-green-400',
|
|
minimapColor: '#22c55e',
|
|
},
|
|
}
|
|
|
|
export interface FlowCanvasNodeData {
|
|
node: TreeStructure
|
|
hasChildren: boolean
|
|
isCollapsed: boolean
|
|
hasValidationErrors: boolean
|
|
isNew: boolean
|
|
onToggleCollapse: (nodeId: string) => void
|
|
}
|
|
|
|
function FlowCanvasNodeComponent({ data, selected }: NodeProps) {
|
|
const { node, hasChildren, isCollapsed, hasValidationErrors, isNew, onToggleCollapse } = data as unknown as FlowCanvasNodeData
|
|
const nodeType = node.type as Exclude<NodeType, 'answer'>
|
|
const config = NODE_TYPE_CONFIG[nodeType] ?? NODE_TYPE_CONFIG.decision
|
|
const Icon = config.icon
|
|
|
|
const title = node.type === 'decision'
|
|
? (node.question || 'Untitled Decision')
|
|
: (node.title || `Untitled ${config.label}`)
|
|
|
|
const optionCount = node.options?.length ?? 0
|
|
|
|
return (
|
|
<>
|
|
{/* Target handle at top */}
|
|
<Handle type="target" position={Position.Top} className="!bg-border !w-2 !h-2 !border-0" />
|
|
|
|
<div
|
|
className={cn(
|
|
'w-[280px] rounded-xl border border-border bg-card shadow-sm cursor-pointer transition-all',
|
|
config.borderClass,
|
|
selected && 'ring-1 ring-primary shadow-md'
|
|
)}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center gap-2 px-3 py-2.5">
|
|
{/* Type badge */}
|
|
<span className={cn('flex h-5 w-5 shrink-0 items-center justify-center rounded', config.badgeClass)}>
|
|
<Icon className="h-3 w-3" />
|
|
</span>
|
|
|
|
{/* Title */}
|
|
<span className="flex-1 truncate text-sm font-heading font-medium text-foreground">
|
|
{title}
|
|
</span>
|
|
|
|
{/* Badges */}
|
|
{isNew && (
|
|
<span className="rounded-full bg-yellow-500/20 px-1.5 py-0.5 text-[10px] font-label text-yellow-400">
|
|
New
|
|
</span>
|
|
)}
|
|
{hasValidationErrors && (
|
|
<AlertCircle className="h-3.5 w-3.5 shrink-0 text-red-400" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Decision options preview */}
|
|
{node.type === 'decision' && optionCount > 0 && (
|
|
<div className="border-t border-border px-3 py-1.5">
|
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
<span className="font-label">{optionCount} option{optionCount !== 1 ? 's' : ''}</span>
|
|
</div>
|
|
<div className="mt-1 space-y-0.5">
|
|
{node.options!.slice(0, 3).map((opt, i) => (
|
|
<div key={opt.id} className="truncate text-xs text-muted-foreground">
|
|
<span className="font-label text-foreground/60">{String.fromCharCode(65 + i)}</span>{' '}
|
|
{opt.label || '(empty)'}
|
|
</div>
|
|
))}
|
|
{optionCount > 3 && (
|
|
<div className="text-xs text-muted-foreground">+{optionCount - 3} more</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description preview for action/solution */}
|
|
{(node.type === 'action' || node.type === 'solution') && node.description && (
|
|
<div className="border-t border-border px-3 py-1.5">
|
|
<div className="line-clamp-2 text-xs text-muted-foreground">{node.description}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Collapse chevron */}
|
|
{hasChildren && (
|
|
<div className="flex justify-center border-t border-border py-1">
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onToggleCollapse(node.id)
|
|
}}
|
|
className="flex items-center gap-1 rounded px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
>
|
|
{isCollapsed ? (
|
|
<>
|
|
<ChevronRight className="h-3 w-3" />
|
|
<span className="font-label">Expand</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<ChevronDown className="h-3 w-3" />
|
|
<span className="font-label">Collapse</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Source handle at bottom */}
|
|
<Handle type="source" position={Position.Bottom} className="!bg-border !w-2 !h-2 !border-0" />
|
|
</>
|
|
)
|
|
}
|
|
|
|
export const FlowCanvasNode = memo(FlowCanvasNodeComponent)
|
|
export { NODE_TYPE_CONFIG }
|