contex improvements

This commit is contained in:
2026-03-07 22:10:16 +01:00
parent 39719b1823
commit 848305fd87
9 changed files with 246 additions and 83 deletions

View File

@@ -32,13 +32,13 @@ export function BaseNode({
// and would be clipped. The inner content wrapper has overflow-hidden for scrolling.
const appliedStyle = hasSize
? {
...style,
width: dimensions.width,
height: dimensions.height,
display: "flex" as const,
flexDirection: "column" as const,
contain: "layout" as const,
}
...style,
width: dimensions.width,
height: dimensions.height,
display: "flex" as const,
flexDirection: "column" as const,
contain: "layout" as const,
}
: style
? { ...style, display: "flex", flexDirection: "column" as const }
: undefined;
@@ -184,7 +184,7 @@ export function BaseNodeContent({
return (
<div
data-slot="base-node-content"
className={cn("min-h-0 flex-1 flex flex-col gap-y-2 overflow-auto pt-1", className)}
className={cn("min-h-0 flex-1 flex flex-col overflow-auto pt-1", className)}
{...props}
/>
);

View File

@@ -13,18 +13,15 @@ import {
BaseNodeFooterText,
BaseNodeHeaderRow,
} from './BaseNode'
import { GitBranchPlus, ScrollText } from 'lucide-react'
import { ScrollText } from 'lucide-react'
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from '../ui/menubar'
import { InputHandle, OutputHandle } from './NodeHandles'
import { NodeMenubar } from './NodeMenubar'
type Props = {
id: string
@@ -159,14 +156,13 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
<BaseNodeHeaderRow icon={<ScrollText className="size-4" />} title={`${id}.puml`} />
<BaseNodeContent>
{hasDependencies && (
<div className="shrink-0 w-full">
<Menubar className="h-auto bg-none p-1 border-none shadow-none">
<MenubarMenu>
<MenubarTrigger className="px-1.5 py-0 text-xs">
<GitBranchPlus className="size-3.5" />
</MenubarTrigger>
<MenubarContent className="min-w-[12rem]">
<div className="shrink-0 w-full">
<NodeMenubar
nodeId={id}
nodeType="config"
editInputsContent={
hasDependencies ? (
<>
{connectedConfigNodes.map((n: any) => (
<MenubarSub key={`config-${n.id}`}>
<MenubarSubTrigger className="text-xs">
@@ -224,11 +220,11 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }:
</MenubarSubContent>
</MenubarSub>
))}
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
)}
</>
) : undefined
}
/>
</div>
<div ref={editorContainerRef} className="min-h-0 flex-1 w-full nodrag nopan overflow-hidden rounded border border-input">
<CodeMirror

View File

@@ -12,6 +12,7 @@ import {
BaseNodeHeaderRow,
} from './BaseNode'
import { InputHandle, OutputHandle } from './NodeHandles'
import { NodeMenubar } from './NodeMenubar'
import { Code2 } from 'lucide-react'
type Props = {
@@ -66,6 +67,9 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height
<BaseNodeHeaderRow icon={<Code2 className="size-4" />} title={id} />
<BaseNodeContent>
<div className="shrink-0 w-full mb-1">
<NodeMenubar nodeId={id} nodeType="function" />
</div>
<p className="shrink-0 text-[10px] text-muted-foreground mb-1">
Call in config: <code className="rounded bg-muted px-0.5">$&#123;{id}(var1, var2)&#125;</code>
</p>

View File

@@ -0,0 +1,112 @@
import React, { useCallback, useContext } from 'react'
import FlowContext from '../../lib/flowContext'
import { DEFAULT_NODE_STYLE, genId, getDefaultDataForType } from '../../lib/flowUtils'
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from '../ui/menubar'
const DUPLICATE_OFFSET = { x: 30, y: 30 }
type NodeType = 'config' | 'render' | 'variable' | 'function'
type Props = {
nodeId: string
nodeType: NodeType
/** Content for Edit → Inputs (config and function nodes only) */
editInputsContent?: React.ReactNode
}
export function NodeMenubar({ nodeId, nodeType, editInputsContent }: Props) {
const ctx = useContext(FlowContext)
const nodes = ctx?.nodes ?? []
const setNodes = ctx?.setNodes
const setEdges = ctx?.setEdges
const edges = ctx?.edges ?? []
const node = nodes.find((n: any) => n.id === nodeId)
const hasEdit = nodeType === 'config' || nodeType === 'function'
const hasConnectedNodes = edges.some((e: any) => e.target === nodeId)
const onDuplicate = useCallback(() => {
if (!setNodes || !node) return
const newId = genId()
const pos = node.position ?? { x: 0, y: 0 }
const newNode = {
id: newId,
type: node.type,
position: { x: pos.x + DUPLICATE_OFFSET.x, y: pos.y + DUPLICATE_OFFSET.y },
data: typeof node.data === 'object' && node.data !== null ? { ...node.data } : node.data,
style: DEFAULT_NODE_STYLE[nodeType] ?? DEFAULT_NODE_STYLE.config,
}
if (newNode.data?.title && nodeType === 'config') newNode.data.title = `config-${newId}`
setNodes((nds: any[]) => nds.concat(newNode))
}, [node, nodeType, setNodes])
const onCopy = useCallback(() => {
if (!node) return
const copy = { id: node.id, type: node.type, data: node.data, position: node.position, style: node.style }
navigator.clipboard?.writeText(JSON.stringify(copy)).catch(() => {})
}, [node])
const onReset = useCallback(() => {
if (!setNodes) return
const defaultData = getDefaultDataForType(nodeType, nodeId)
setNodes((nds: any[]) =>
nds.map((n) => (n.id === nodeId ? { ...n, data: defaultData } : n))
)
}, [nodeId, nodeType, setNodes])
const onDelete = useCallback(() => {
if (!setNodes || !setEdges) return
setNodes((nds: any[]) => nds.filter((n: any) => n.id !== nodeId))
setEdges((eds: any[]) => eds.filter((e: any) => e.source !== nodeId && e.target !== nodeId))
}, [nodeId, setNodes, setEdges])
return (
<Menubar className="h-auto min-h-0 flex items-center bg-none p-1 border-none shadow-none">
<MenubarMenu>
<MenubarTrigger className="px-1.5 py-0 text-xs">
File
</MenubarTrigger>
<MenubarContent className="min-w-[10rem]">
<MenubarItem className="text-xs" onClick={onDuplicate}>
Duplicate
</MenubarItem>
<MenubarItem className="text-xs" onClick={onCopy}>
Copy
</MenubarItem>
<MenubarItem className="text-xs" onClick={onReset}>
Reset
</MenubarItem>
<MenubarItem className="text-xs text-destructive focus:text-destructive" onClick={onDelete}>
Delete
</MenubarItem>
</MenubarContent>
</MenubarMenu>
{hasEdit && (
<MenubarMenu>
<MenubarTrigger className="px-1.5 py-0 text-xs">
Edit
</MenubarTrigger>
<MenubarContent className="min-w-[10rem]">
<MenubarSub>
<MenubarSubTrigger className="text-xs" disabled={!hasConnectedNodes}>
Inputs
</MenubarSubTrigger>
<MenubarSubContent className="min-w-[12rem]">
{editInputsContent}
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
)}
</Menubar>
)
}

View File

@@ -10,6 +10,7 @@ import {
BaseNodeFooterText,
BaseNodeHeaderRow,
} from './BaseNode'
import { NodeMenubar } from './NodeMenubar'
import { Sparkles } from 'lucide-react'
import { InputHandle, OutputHandle } from './NodeHandles'
@@ -297,6 +298,9 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }:
<BaseNodeHeaderRow icon={<Sparkles className="size-4" />} title={id} />
<BaseNodeContent>
<div className="shrink-0 w-full">
<NodeMenubar nodeId={id} nodeType="render" />
</div>
<div className="min-h-0 flex-1 flex flex-col">
{incomingIds.length === 0 ? (
<Empty className="min-h-0 flex-1">

View File

@@ -7,6 +7,7 @@ import {
BaseNodeFooterText,
BaseNodeHeaderRow,
} from './BaseNode'
import { NodeMenubar } from './NodeMenubar'
import { Input } from '../ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { Switch } from '../ui/switch'
@@ -93,6 +94,9 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) {
<BaseNodeHeaderRow icon={<Variable className="size-4" />} title={id} />
<BaseNodeContent className="gap-2 p-3">
<div className="shrink-0 w-full">
<NodeMenubar nodeId={id} nodeType="variable" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground">Type</label>
<Select value={valueType} onValueChange={onTypeChange}>