fix styles and menu

This commit is contained in:
2026-03-06 00:39:26 +01:00
parent 417999d4d8
commit 65b7cb37fe
11 changed files with 1232 additions and 101 deletions

View File

@@ -18,7 +18,14 @@ import CustomNode from './components/CustomNode'
import ConfigNode from './components/ConfigNode'
import RenderingNode from './components/RenderingNode'
import FlowContext from './lib/flowContext'
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
// Generate short unique node ids when missing
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
@@ -45,7 +52,8 @@ export default function App() {
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
const wrapperRef = React.useRef<HTMLDivElement | null>(null)
const [contextMenu, setContextMenu] = React.useState<null | { x: number; y: number; clientX: number; clientY: number }>(null)
const lastClickRef = React.useRef<{ clientX: number; clientY: number } | null>(null)
const [contextTarget, setContextTarget] = React.useState<null | { type: 'canvas' | 'node'; nodeId?: string; clientX: number; clientY: number }>(null)
const nodeTypes = React.useMemo(
() => ({ custom: CustomNode, config: ConfigNode, render: RenderingNode }),
@@ -61,26 +69,31 @@ export default function App() {
setRfInstance(instance)
}, [])
const hideMenu = React.useCallback(() => setContextMenu(null), [])
const onCanvasContextMenu = React.useCallback((ev: React.MouseEvent) => {
ev.preventDefault()
const target = ev.target as HTMLElement
// don't show menu when right-clicking nodes or handles
if (target.closest('.react-flow__node') || target.closest('.react-flow__handle')) return
const rect = wrapperRef.current?.getBoundingClientRect()
const nodeEl = target.closest('.react-flow__node') as HTMLElement | null
const handleEl = target.closest('.react-flow__handle') as HTMLElement | null
const clientX = ev.clientX
const clientY = ev.clientY
const x = rect ? clientX - rect.left : ev.clientX
const y = rect ? clientY - rect.top : ev.clientY
setContextMenu({ x, y, clientX, clientY })
lastClickRef.current = { clientX, clientY }
if (nodeEl && !handleEl) {
const nodeId = nodeEl.dataset?.id || nodeEl.getAttribute('data-id') || (nodeEl.id && nodeEl.id.startsWith('reactflow__node-') ? nodeEl.id.replace('reactflow__node-', '') : undefined)
setContextTarget({ type: 'node', nodeId, clientX, clientY })
return
}
setContextTarget({ type: 'canvas', clientX, clientY })
}, [])
const createNode = React.useCallback(
(type: string) => {
if (!rfInstance) return
if (!contextMenu) return
const { clientX, clientY } = contextMenu
const click = contextTarget ?? lastClickRef.current
const clientX = click?.clientX ?? window.innerWidth / 2
const clientY = click?.clientY ?? window.innerHeight / 2
const rect = wrapperRef.current?.getBoundingClientRect()
const point = rect ? { x: clientX - rect.left, y: clientY - rect.top } : { x: clientX, y: clientY }
let position = point
@@ -99,52 +112,60 @@ export default function App() {
data: type === 'config' ? { yaml: '# Enter YAML here\n', title: `config-${id}` } : {},
}
setNodes((nds) => nds.concat(newNode))
hideMenu()
lastClickRef.current = null
setContextTarget(null)
},
[rfInstance, contextMenu, hideMenu, setNodes]
[rfInstance, setNodes]
)
const deleteNode = React.useCallback((id: string | undefined) => {
if (!id) return
setNodes((nds) => nds.filter((n) => n.id !== id))
setEdges((eds) => eds.filter((e) => e.source !== id && e.target !== id))
setContextTarget(null)
}, [setNodes, setEdges])
return (
<div className="reactflow-wrapper" ref={wrapperRef} onContextMenu={onCanvasContextMenu} style={{ position: 'relative' }}>
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
onInit={onInit}
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
{contextMenu && (
<div
className="absolute bg-white border rounded shadow-md text-sm"
style={{ left: contextMenu.x, top: contextMenu.y, zIndex: 9999 }}
onMouseLeave={hideMenu}
>
<div className="p-2">
<div className="text-xs text-gray-700 mb-2">Create node</div>
<div className="space-y-1">
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('config')}>
Configuration Node
</button>
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('render')}>
Rendering Node
</button>
<button className="w-full text-left px-2 py-1 hover:bg-gray-100" onClick={() => createNode('custom')}>
Custom Node
</button>
</div>
<ContextMenu>
<ContextMenuTrigger asChild>
<div style={{ width: '100%', height: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
onInit={onInit}
>
<Background />
<Controls />ß
<MiniMap />
</ReactFlow>
</div>
</div>
)}
</ContextMenuTrigger>
<ContextMenuContent>
{contextTarget?.type === 'node' ? (
<ContextMenuItem onSelect={() => deleteNode(contextTarget.nodeId)}>Delete</ContextMenuItem>
) : (
<>
<ContextMenuGroup>
<ContextMenuLabel>New Node</ContextMenuLabel>
<ContextMenuItem onSelect={() => createNode('config')}>Config</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('render')}>Renderer</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('custom')}>Custom</ContextMenuItem>
</ContextMenuGroup>
</>
)}
</ContextMenuContent>
</ContextMenu>
</FlowContext.Provider>
</div>
)
}