diff --git a/src/components/graph/BaseNode.tsx b/src/components/graph/BaseNode.tsx index bb4631b..dc1301b 100644 --- a/src/components/graph/BaseNode.tsx +++ b/src/components/graph/BaseNode.tsx @@ -1,6 +1,5 @@ import type { ComponentProps, ReactNode } from "react"; import { NodeResizeControl } from "@xyflow/react"; -import { Expand } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -11,20 +10,38 @@ export type BaseNodeProps = ComponentProps<"div"> & { nodeId?: string; /** Connection handles (InputHandle, OutputHandle). Rendered outside the overflow layer so they stay visible. */ handles?: ReactNode; + /** When provided, node uses this size (e.g. from React Flow width/height). Enables correct resize behavior. */ + dimensions?: { width: number; height: number }; }; export function BaseNode({ className, style, + dimensions, resizable, nodeId, handles, children, ...props }: BaseNodeProps) { - const appliedStyle = style - ? { ...style, display: "flex", flexDirection: "column" as const } - : undefined; + const hasSize = + dimensions && + dimensions.width > 0 && + dimensions.height > 0; + // Don't set overflow: hidden on the root — handles are positioned at left/right -10px + // 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 + ? { ...style, display: "flex", flexDirection: "column" as const } + : undefined; return (
-
{children}
+
+ {children} +
{resizable && nodeId && ( -
- - - - - -
+ + )} {handles}
@@ -110,6 +130,53 @@ export function BaseNodeHeaderTitle({ ); } +/** + * Standard node header: icon on the left, title taking the rest. Use for all node types. + * Inlined so it does not depend on BaseNodeHeader/BaseNodeHeaderTitle (avoids reference errors with HMR). + */ +export function BaseNodeHeaderRow({ + icon, + title, + className, +}: { + icon: ReactNode; + title: ReactNode; + className?: string; +}) { + return ( +
+ {icon} +

+ {title} +

+
+ ); +} + +/** + * Single-line muted text for the footer. Use for status or hints in all node types. + */ +export function BaseNodeFooterText({ + className, + ...props +}: ComponentProps<"div">) { + return ( +
+ ); +} + export function BaseNodeContent({ className, ...props diff --git a/src/components/graph/ConfigNode.tsx b/src/components/graph/ConfigNode.tsx index 4477605..b1c9a7a 100644 --- a/src/components/graph/ConfigNode.tsx +++ b/src/components/graph/ConfigNode.tsx @@ -8,8 +8,8 @@ import { BaseNode, BaseNodeContent, BaseNodeFooter, - BaseNodeHeader, - BaseNodeHeaderTitle, + BaseNodeFooterText, + BaseNodeHeaderRow, } from './BaseNode' import { GitBranchPlus, ScrollText } from 'lucide-react' import { @@ -119,12 +119,14 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: const extensions = useMemo(() => [plantumlLanguage.extension], []) const [editorHeight, editorContainerRef] = useResizeHeight(180) + const dimensions = + width != null && height != null && width > 0 && height > 0 + ? { width, height } + : undefined + return ( - 0 && height > 0 ? { width, height } : undefined} resizable nodeId={id} handles={<>}> - - - {id}.puml - + }> + } title={`${id}.puml`} /> {hasDependencies && ( @@ -214,7 +216,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: -
{storedPlantuml ? `${storedPlantuml.length} chars` : 'none'}
+ {storedPlantuml ? `${storedPlantuml.length} chars` : 'none'}
) diff --git a/src/components/graph/FunctionNode.tsx b/src/components/graph/FunctionNode.tsx index 9cfb7ac..31404cf 100644 --- a/src/components/graph/FunctionNode.tsx +++ b/src/components/graph/FunctionNode.tsx @@ -8,8 +8,8 @@ import { BaseNode, BaseNodeContent, BaseNodeFooter, - BaseNodeHeader, - BaseNodeHeaderTitle, + BaseNodeFooterText, + BaseNodeHeaderRow, } from './BaseNode' import { InputHandle, OutputHandle } from './NodeHandles' import { Code2 } from 'lucide-react' @@ -17,7 +17,8 @@ import { Code2 } from 'lucide-react' type Props = { id: string data: { body?: string } - style?: React.CSSProperties + width?: number + height?: number } const DEFAULT_BODY = `// args[0], args[1], ... are the variable values (in order) @@ -55,13 +56,14 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height const extensions = useMemo(() => [javascript()], []) const [editorHeight, editorContainerRef] = useResizeHeight(120) + const dimensions = + width != null && height != null && width > 0 && height > 0 + ? { width, height } + : undefined return ( - 0 && height > 0 ? { width, height } : undefined} resizable nodeId={id} handles={<>}> - - - {id} - + }> + } title={id} />

@@ -81,9 +83,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data, width, height -

- Body: {storedBody ? `${storedBody.length} chars` : 'none'} -
+ Body: {storedBody ? `${storedBody.length} chars` : 'none'}
) diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index eb8a900..3009060 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -6,8 +6,8 @@ import { BaseNode, BaseNodeContent, BaseNodeFooter, - BaseNodeHeader, - BaseNodeHeaderTitle, + BaseNodeFooterText, + BaseNodeHeaderRow, } from './BaseNode' import { Sparkles } from 'lucide-react' import { InputHandle, OutputHandle } from './NodeHandles' @@ -266,12 +266,14 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: // Do not depend on nodes/edges refs to avoid flicker from unnecessary re-renders. }, [id, theme, plantumlText, configSignature, edgesSignature, variablesSignature, functionsSignature]) + const dimensions = + width != null && height != null && width > 0 && height > 0 + ? { width, height } + : undefined + return ( - 0 && height > 0 ? { width, height } : undefined} resizable nodeId={id} handles={<>}> - - - {id} - + }> + } title={id} />
@@ -324,9 +326,9 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: -
+ {svgContent ? 'PlantUML diagram' : error ? 'Error' : '—'} -
+
) diff --git a/src/components/graph/VariableNode.tsx b/src/components/graph/VariableNode.tsx index 3fc9559..63afdce 100644 --- a/src/components/graph/VariableNode.tsx +++ b/src/components/graph/VariableNode.tsx @@ -3,8 +3,9 @@ import FlowContext from '../../lib/flowContext' import { BaseNode, BaseNodeContent, - BaseNodeHeader, - BaseNodeHeaderTitle, + BaseNodeFooter, + BaseNodeFooterText, + BaseNodeHeaderRow, } from './BaseNode' import { Input } from '../ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' @@ -89,10 +90,7 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) { return ( }> - - - {id} - + } title={id} />
@@ -126,10 +124,13 @@ export const VariableNode = memo(function VariableNode({ id, data }: Props) { /> )}
-

- Use in config: prop: ${{id}} -

+ + + + Use in config: prop: {'${'}{id}{'}'} + +
) }) diff --git a/src/styles.css b/src/styles.css index 9ef5f24..30f6ec6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -51,6 +51,18 @@ body { } } +/* Resize control: snappy drag, no transition or selection delay */ +.react-flow__resize-control.bottom.right { + cursor: se-resize; + touch-action: none; + user-select: none; +} + +.react-flow__resize-control.bottom.right, +.react-flow__resize-control.bottom.right * { + transition: none; +} + /* Small helper for monospace pre output */ pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;