improve menus

This commit is contained in:
2026-03-07 21:49:15 +01:00
parent 65d4f216a8
commit 39719b1823
2 changed files with 64 additions and 44 deletions

View File

@@ -23,13 +23,15 @@ import { useTheme } from './lib/themeContext'
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuGroup,
ContextMenuItem, ContextMenuItem,
ContextMenuLabel, ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger, ContextMenuTrigger,
ContextMenuGroup
} from "@/components/ui/context-menu" } from "@/components/ui/context-menu"
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Moon, Sun } from 'lucide-react' import { Code2, Moon, ScrollText, Sparkles, Sun, Variable } from 'lucide-react'
// Generate short unique node ids when missing // Generate short unique node ids when missing
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}` const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
@@ -118,14 +120,13 @@ export default function App() {
const click = contextTarget ?? lastClickRef.current const click = contextTarget ?? lastClickRef.current
const clientX = click?.clientX ?? window.innerWidth / 2 const clientX = click?.clientX ?? window.innerWidth / 2
const clientY = click?.clientY ?? window.innerHeight / 2 const clientY = click?.clientY ?? window.innerHeight / 2
const rect = wrapperRef.current?.getBoundingClientRect() let position: { x: number; y: number }
const point = rect ? { x: clientX - rect.left, y: clientY - rect.top } : { x: clientX, y: clientY }
let position = point
try { try {
// project to flow coords when possible // Convert screen (client) coordinates to flow coordinates — same position as where the context menu opened
position = rfInstance.project ? rfInstance.project(point) : point const screenToFlow = rfInstance.screenToFlowPosition ?? rfInstance.project
} catch (e) { position = screenToFlow.call(rfInstance, { x: clientX, y: clientY })
// ignore and use raw point } catch {
position = { x: clientX, y: clientY }
} }
position = snapToGrid(position.x, position.y) position = snapToGrid(position.x, position.y)
@@ -154,7 +155,7 @@ export default function App() {
lastClickRef.current = null lastClickRef.current = null
setContextTarget(null) setContextTarget(null)
}, },
[rfInstance, setNodes] [rfInstance, setNodes, contextTarget]
) )
const deleteNode = React.useCallback((id: string | undefined) => { const deleteNode = React.useCallback((id: string | undefined) => {
@@ -182,46 +183,64 @@ export default function App() {
<div style={{ width: '100%', height: '100%' }}> <div style={{ width: '100%', height: '100%' }}>
<ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView> <ReactFlowProvider initialNodes={nodes} initialEdges={edges} fitView>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
defaultEdgeOptions={{ type: 'animated' }} defaultEdgeOptions={{ type: 'animated' }}
colorMode={theme as ColorMode} colorMode={theme as ColorMode}
snapToGrid snapToGrid
snapGrid={SNAP_GRID} snapGrid={SNAP_GRID}
fitView fitView
onInit={onInit} onInit={onInit}
> >
<Background variant="dots" gap={20} /> <Background variant="dots" gap={20} />
<Controls /> <Controls />
<MiniMap /> <MiniMap />
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent className="w-48">
{contextTarget?.type === 'node' ? ( {contextTarget?.type === 'node' ? (
<ContextMenuItem onSelect={() => deleteNode(contextTarget.nodeId)}>Delete</ContextMenuItem> <ContextMenuGroup>
<ContextMenuItem onSelect={() => deleteNode(contextTarget.nodeId)}>Delete</ContextMenuItem>
</ContextMenuGroup>
) : ( ) : (
<> <ContextMenuGroup>
<ContextMenuGroup> <ContextMenuSub>
<ContextMenuLabel>New Node</ContextMenuLabel> <ContextMenuSubTrigger>Create node</ContextMenuSubTrigger>
<ContextMenuItem onSelect={() => createNode('config')}>Config</ContextMenuItem> <ContextMenuSubContent className="w-44">
<ContextMenuItem onSelect={() => createNode('render')}>Renderer</ContextMenuItem> <ContextMenuGroup>
<ContextMenuItem onSelect={() => createNode('variable')}>Variable</ContextMenuItem> <ContextMenuItem onSelect={() => createNode('config')}>
<ContextMenuItem onSelect={() => createNode('function')}>Function</ContextMenuItem> <ScrollText className="mr-2 h-4 w-4" />
</ContextMenuGroup> Config
</> </ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('render')}>
<Sparkles className="mr-2 h-4 w-4" />
Renderer
</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('variable')}>
<Variable className="mr-2 h-4 w-4" />
Variable
</ContextMenuItem>
<ContextMenuItem onSelect={() => createNode('function')}>
<Code2 className="mr-2 h-4 w-4" />
Function
</ContextMenuItem>
</ContextMenuGroup>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuGroup>
)} )}
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
</FlowContext.Provider> </FlowContext.Provider>
</div> </div >
) )
} }

View File

@@ -44,11 +44,12 @@ body {
@keyframes edge-flow { @keyframes edge-flow {
from { from {
stroke-dashoffset: 0;
}
to {
stroke-dashoffset: 14; stroke-dashoffset: 14;
} }
to {
stroke-dashoffset: 0;
}
} }
/* Resize control: snappy drag, no transition or selection delay */ /* Resize control: snappy drag, no transition or selection delay */