From 8d9964c6448443365417fa3cf49e445d04061203 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 13 Mar 2026 10:58:20 +1300 Subject: [PATCH 01/11] fix: use overflow-visible on non-fullBleed nodes to prevent handle clipping The default content div used overflow-hidden which clipped handles positioned at left: -7px / right: -7px. Nodes that need clipping (ImageInput, Annotation, VideoTrim) already set their own contentClassName. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/BaseNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index a5c70096..4f296622 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -305,7 +305,7 @@ export function BaseNode({ setHoveredNodeId(null); }} > -
{children}
+
{children}
{settingsPanel && (
From 61af53b2b8dcca82334967c7e69aba9112500457 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 13 Mar 2026 10:58:27 +1300 Subject: [PATCH 02/11] fix: resolve prompt variables through router nodes for PromptConstructor Variables from upstream Prompt nodes weren't discovered when a Router sat between the Prompt and PromptConstructor. Added resolveTextSourcesThroughRouters() helper that recursively traverses router nodes to find actual text sources, used in both the component and executor. Co-Authored-By: Claude Opus 4.6 --- .../nodes/PromptConstructorNode.tsx | 4 ++- src/store/execution/simpleNodeExecutors.ts | 6 ++-- src/store/utils/connectedInputs.ts | 35 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/components/nodes/PromptConstructorNode.tsx b/src/components/nodes/PromptConstructorNode.tsx index eaf7676d..3ee9b4f9 100644 --- a/src/components/nodes/PromptConstructorNode.tsx +++ b/src/components/nodes/PromptConstructorNode.tsx @@ -6,6 +6,7 @@ import { BaseNode } from "./BaseNode"; import { usePromptAutocomplete } from "@/hooks/usePromptAutocomplete"; import { useWorkflowStore } from "@/store/workflowStore"; import { PromptConstructorNodeData, PromptNodeData, LLMGenerateNodeData, AvailableVariable } from "@/types"; +import { resolveTextSourcesThroughRouters } from "@/store/utils/connectedInputs"; import { parseVarTags } from "@/utils/parseVarTags"; type PromptConstructorNodeType = Node; @@ -31,10 +32,11 @@ export function PromptConstructorNode({ id, data, selected }: NodeProps tags) const availableVariables = useMemo((): AvailableVariable[] => { - const connectedTextNodes = edges + const directTextNodes = edges .filter((e) => e.target === id && e.targetHandle === "text") .map((e) => nodes.find((n) => n.id === e.source)) .filter((n): n is typeof nodes[0] => n !== undefined); + const connectedTextNodes = resolveTextSourcesThroughRouters(directTextNodes, nodes, edges); const vars: AvailableVariable[] = []; const usedNames = new Set(); diff --git a/src/store/execution/simpleNodeExecutors.ts b/src/store/execution/simpleNodeExecutors.ts index 8adf593a..137c2e72 100644 --- a/src/store/execution/simpleNodeExecutors.ts +++ b/src/store/execution/simpleNodeExecutors.ts @@ -18,6 +18,7 @@ import type { WorkflowNode, } from "@/types"; import type { NodeExecutionContext } from "./types"; +import { resolveTextSourcesThroughRouters } from "@/store/utils/connectedInputs"; import { parseTextToArray } from "@/utils/arrayParser"; import { parseVarTags } from "@/utils/parseVarTags"; @@ -109,11 +110,12 @@ export async function executePromptConstructor(ctx: NodeExecutionContext): Promi const edges = getEdges(); const nodes = getNodes(); - // Find all connected text nodes - const connectedTextNodes = edges + // Find all connected text nodes (resolving through routers) + const directTextNodes = edges .filter((e) => e.target === node.id && e.targetHandle === "text") .map((e) => nodes.find((n) => n.id === e.source)) .filter((n): n is WorkflowNode => n !== undefined); + const connectedTextNodes = resolveTextSourcesThroughRouters(directTextNodes, nodes, edges); // Build variable map: named variables from Prompt nodes take precedence const variableMap: Record = {}; diff --git a/src/store/utils/connectedInputs.ts b/src/store/utils/connectedInputs.ts index a848a380..2c8b817f 100644 --- a/src/store/utils/connectedInputs.ts +++ b/src/store/utils/connectedInputs.ts @@ -119,6 +119,41 @@ function getSourceOutput( return { type: "image", value: null }; } +/** + * Resolves text source nodes through router (passthrough) nodes. + * Given a list of directly-connected source nodes, expands any router nodes + * by recursively following their upstream text connections to find the actual + * text-producing source nodes. + */ +export function resolveTextSourcesThroughRouters( + sourceNodes: WorkflowNode[], + allNodes: WorkflowNode[], + edges: { source: string; target: string; targetHandle?: string | null }[], + visited?: Set +): WorkflowNode[] { + const seen = visited ?? new Set(); + const resolved: WorkflowNode[] = []; + + for (const node of sourceNodes) { + if (seen.has(node.id)) continue; + seen.add(node.id); + + if (node.type === "router") { + const upstreamNodes = edges + .filter((e) => e.target === node.id && e.targetHandle === "text") + .map((e) => allNodes.find((n) => n.id === e.source)) + .filter((n): n is WorkflowNode => n !== undefined); + resolved.push( + ...resolveTextSourcesThroughRouters(upstreamNodes, allNodes, edges, seen) + ); + } else { + resolved.push(node); + } + } + + return resolved; +} + /** * Get all connected inputs for a node. * Pure function version of workflowStore.getConnectedInputs. From 2abfe0452586671f41dd306b4073ff5c2f9c8623 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 13 Mar 2026 10:59:31 +1300 Subject: [PATCH 03/11] fix: update WelcomeModal test to match bg-black/60 backdrop class Co-Authored-By: Claude Opus 4.6 --- src/components/__tests__/WelcomeModal.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/__tests__/WelcomeModal.test.tsx b/src/components/__tests__/WelcomeModal.test.tsx index 129e0e28..deb5560c 100644 --- a/src/components/__tests__/WelcomeModal.test.tsx +++ b/src/components/__tests__/WelcomeModal.test.tsx @@ -81,7 +81,7 @@ describe("WelcomeModal", () => { /> ); - const backdrop = container.querySelector(".bg-black\\/50"); + const backdrop = container.querySelector(".bg-black\\/60"); expect(backdrop).toBeInTheDocument(); }); }); From 0fd65682cece073f8c83d6e02f5f12ef5fa4ee78 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 22 Mar 2026 20:40:40 +1300 Subject: [PATCH 04/11] fix: prevent node height accumulation with inline parameters on reload Store measured settings panel height in node data (_settingsPanelHeight). On workflow load, subtract it from persisted height so BaseNode's expand effect re-adds the real panel height from a clean baseline instead of double-counting. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/BaseNode.tsx | 10 ++++-- src/store/workflowStore.ts | 53 ++++++++++++++++++++++++++----- src/types/nodes.ts | 5 +++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index 4f296622..ab1ed2bb 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -141,7 +141,10 @@ export function BaseNode({ nodes.map((node) => { if (node.id !== id) return node; const currentHeight = getNodeDimension(node, "height"); - return applyNodeDimensions(node, getNodeDimension(node, "width"), currentHeight + finalHeight); + return { + ...applyNodeDimensions(node, getNodeDimension(node, "width"), currentHeight + finalHeight), + data: { ...node.data, _settingsPanelHeight: finalHeight }, + }; }) ); } @@ -188,7 +191,10 @@ export function BaseNode({ if (node.id !== id) return node; const currentHeight = getNodeDimension(node, "height"); const newHeight = Math.max(minHeight, currentHeight + delta); - return applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight); + return { + ...applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight), + data: { ...node.data, _settingsPanelHeight: newPanelHeight }, + }; }) ); diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 3391afde..5fa80861 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -1712,17 +1712,54 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ // Load cost data for this workflow const costData = workflow.id ? loadWorkflowCostData(workflow.id) : null; + // Subtract saved settings panel height so BaseNode's expand effect + // re-adds the real panel height from a clean baseline (prevents + // height accumulation on each save/reload cycle). + const INLINE_PARAM_NODE_TYPES = new Set([ + "nanoBanana", "generateVideo", "generate3d", "generateAudio", "llmGenerate", + ]); + const inlineParamsEnabled = (() => { + try { return localStorage.getItem("node-banana-inline-parameters") === "true"; } + catch { return false; } + })(); + set({ // Clear selected state - selection should not be persisted across sessions // Also validate position to ensure coordinates are finite numbers - nodes: hydratedWorkflow.nodes.map(node => ({ - ...node, - selected: false, - position: { - x: isFinite(node.position?.x) ? node.position.x : 0, - y: isFinite(node.position?.y) ? node.position.y : 0, - }, - })), + nodes: hydratedWorkflow.nodes.map(node => { + let adjustedNode = { + ...node, + selected: false, + position: { + x: isFinite(node.position?.x) ? node.position.x : 0, + y: isFinite(node.position?.y) ? node.position.y : 0, + }, + }; + + // Subtract panel height for inline-param nodes so the expand effect + // can re-add the real measured height without double-counting + const data = node.data as Record; + const panelHeight = typeof data?._settingsPanelHeight === "number" ? data._settingsPanelHeight : 0; + if ( + INLINE_PARAM_NODE_TYPES.has(node.type ?? "") && + inlineParamsEnabled && + (data?.parametersExpanded ?? true) === true && + panelHeight > 0 + ) { + const currentHeight = (node.style?.height as number) ?? (defaultNodeDimensions[node.type as NodeType]?.height ?? 300); + const minHeight = defaultNodeDimensions[node.type as NodeType]?.height ?? 300; + const newHeight = Math.max(minHeight, currentHeight - panelHeight); + adjustedNode = { + ...adjustedNode, + width: undefined, + height: undefined, + measured: undefined, + style: { ...adjustedNode.style, height: newHeight }, + } as typeof adjustedNode; + } + + return adjustedNode; + }), edges: hydratedWorkflow.edges, edgeStyle: hydratedWorkflow.edgeStyle || "angular", groups: hydratedWorkflow.groups || {}, diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 963c41f7..5a7681c9 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -178,6 +178,7 @@ export interface NanoBananaNodeData extends BaseNodeData { parameters?: Record; // Model-specific parameters for external providers inputSchema?: ModelInputDef[]; // Model's input schema for dynamic handles parametersExpanded?: boolean; // Collapse state for inline parameter display + _settingsPanelHeight?: number; // Measured settings panel height for reload correction status: NodeStatus; error: string | null; imageHistory: CarouselImageItem[]; // Carousel history (IDs only) @@ -197,6 +198,7 @@ export interface GenerateVideoNodeData extends BaseNodeData { parameters?: Record; // Model-specific parameters inputSchema?: ModelInputDef[]; // Model's input schema for dynamic handles parametersExpanded?: boolean; // Collapse state for inline parameter display + _settingsPanelHeight?: number; // Measured settings panel height for reload correction status: NodeStatus; error: string | null; videoHistory: CarouselVideoItem[]; // Carousel history (IDs only) @@ -217,6 +219,7 @@ export interface Generate3DNodeData extends BaseNodeData { parameters?: Record; inputSchema?: ModelInputDef[]; parametersExpanded?: boolean; // Collapse state for inline parameter display + _settingsPanelHeight?: number; // Measured settings panel height for reload correction status: NodeStatus; error: string | null; } @@ -242,6 +245,7 @@ export interface GenerateAudioNodeData extends BaseNodeData { parameters?: Record; // Model-specific parameters (voice, speed, etc.) inputSchema?: ModelInputDef[]; // Model's input schema for dynamic handles parametersExpanded?: boolean; // Collapse state for inline parameter display + _settingsPanelHeight?: number; // Measured settings panel height for reload correction status: NodeStatus; error: string | null; audioHistory: CarouselAudioItem[]; // Carousel history (IDs only) @@ -263,6 +267,7 @@ export interface LLMGenerateNodeData extends BaseNodeData { temperature: number; maxTokens: number; parametersExpanded?: boolean; // Collapse state for inline parameter display + _settingsPanelHeight?: number; // Measured settings panel height for reload correction status: NodeStatus; error: string | null; } From f7864197b8ad6e1700d0470b5b386e3dad463291 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 22 Mar 2026 21:02:54 +1300 Subject: [PATCH 05/11] fix: move panel height correction from loadWorkflow into BaseNode render The pre-subtraction in loadWorkflow was overridden by React Flow's dimension reconciliation. Instead, correct at the source: in BaseNode's expand timeout, read _settingsPanelHeight from node data and only add the delta (measured - saved) so reloaded nodes don't double-count. Also clear _settingsPanelHeight on collapse to avoid stale values. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/BaseNode.tsx | 15 +++++++-- src/store/workflowStore.ts | 53 +++++-------------------------- 2 files changed, 20 insertions(+), 48 deletions(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index ab1ed2bb..59a8aed4 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -108,7 +108,10 @@ export function BaseNode({ if (node.id !== id) return node; const currentHeight = getNodeDimension(node, "height"); const newHeight = Math.max(minHeight, currentHeight - heightToRemove); - return applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight); + return { + ...applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight), + data: { ...node.data, _settingsPanelHeight: 0 }, + }; }) ); @@ -134,15 +137,21 @@ export function BaseNode({ animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; - // Apply the final panel height in one shot, then unlock the wrapper + // Apply the final panel height in one shot, then unlock the wrapper. + // Subtract any previously saved panel height to avoid double-counting + // on workflow reload (saved node height already includes panel). const finalHeight = trackedSettingsHeightRef.current; if (finalHeight > 0) { setNodes((nodes) => nodes.map((node) => { if (node.id !== id) return node; + const savedPanelHeight = typeof (node.data as Record)?._settingsPanelHeight === "number" + ? (node.data as Record)._settingsPanelHeight as number + : 0; + const heightToAdd = finalHeight - savedPanelHeight; const currentHeight = getNodeDimension(node, "height"); return { - ...applyNodeDimensions(node, getNodeDimension(node, "width"), currentHeight + finalHeight), + ...applyNodeDimensions(node, getNodeDimension(node, "width"), currentHeight + heightToAdd), data: { ...node.data, _settingsPanelHeight: finalHeight }, }; }) diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 5fa80861..3391afde 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -1712,54 +1712,17 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ // Load cost data for this workflow const costData = workflow.id ? loadWorkflowCostData(workflow.id) : null; - // Subtract saved settings panel height so BaseNode's expand effect - // re-adds the real panel height from a clean baseline (prevents - // height accumulation on each save/reload cycle). - const INLINE_PARAM_NODE_TYPES = new Set([ - "nanoBanana", "generateVideo", "generate3d", "generateAudio", "llmGenerate", - ]); - const inlineParamsEnabled = (() => { - try { return localStorage.getItem("node-banana-inline-parameters") === "true"; } - catch { return false; } - })(); - set({ // Clear selected state - selection should not be persisted across sessions // Also validate position to ensure coordinates are finite numbers - nodes: hydratedWorkflow.nodes.map(node => { - let adjustedNode = { - ...node, - selected: false, - position: { - x: isFinite(node.position?.x) ? node.position.x : 0, - y: isFinite(node.position?.y) ? node.position.y : 0, - }, - }; - - // Subtract panel height for inline-param nodes so the expand effect - // can re-add the real measured height without double-counting - const data = node.data as Record; - const panelHeight = typeof data?._settingsPanelHeight === "number" ? data._settingsPanelHeight : 0; - if ( - INLINE_PARAM_NODE_TYPES.has(node.type ?? "") && - inlineParamsEnabled && - (data?.parametersExpanded ?? true) === true && - panelHeight > 0 - ) { - const currentHeight = (node.style?.height as number) ?? (defaultNodeDimensions[node.type as NodeType]?.height ?? 300); - const minHeight = defaultNodeDimensions[node.type as NodeType]?.height ?? 300; - const newHeight = Math.max(minHeight, currentHeight - panelHeight); - adjustedNode = { - ...adjustedNode, - width: undefined, - height: undefined, - measured: undefined, - style: { ...adjustedNode.style, height: newHeight }, - } as typeof adjustedNode; - } - - return adjustedNode; - }), + nodes: hydratedWorkflow.nodes.map(node => ({ + ...node, + selected: false, + position: { + x: isFinite(node.position?.x) ? node.position.x : 0, + y: isFinite(node.position?.y) ? node.position.y : 0, + }, + })), edges: hydratedWorkflow.edges, edgeStyle: hydratedWorkflow.edgeStyle || "angular", groups: hydratedWorkflow.groups || {}, From e82a23ef3851946143eafc603ef3226ff7c86172 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 22 Mar 2026 21:37:22 +1300 Subject: [PATCH 06/11] fix: replace ArrayNode auto-route icon with Lucide split icon Use the Lucide split icon (rotated 90deg) for the auto-route button to better convey the splitting action. Also improve settings reactivity by reading from Zustand store directly and reparsing atomically on setting changes. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/ArrayNode.tsx | 52 ++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/components/nodes/ArrayNode.tsx b/src/components/nodes/ArrayNode.tsx index ce6ed0db..bce74194 100644 --- a/src/components/nodes/ArrayNode.tsx +++ b/src/components/nodes/ArrayNode.tsx @@ -19,12 +19,18 @@ function arraysEqual(a: string[], b: string[]): boolean { } export function ArrayNode({ id, data, selected }: NodeProps) { - const nodeData = data; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const addNode = useWorkflowStore((state) => state.addNode); const onConnect = useWorkflowStore((state) => state.onConnect); const nodes = useWorkflowStore((state) => state.nodes); const edges = useWorkflowStore((state) => state.edges); + + // Derive nodeData from the Zustand store (already subscribed via `nodes`) + // rather than React Flow props, so settings changes are reflected immediately. + const nodeData = useMemo(() => { + const n = nodes.find((nd) => nd.id === id); + return (n?.data as ArrayNodeData) ?? data; + }, [nodes, id, data]); const { setNodes, getNodes } = useReactFlow(); const lastSyncedInputRef = useRef(null); const lastDerivedWriteRef = useRef(null); @@ -103,11 +109,36 @@ export function ArrayNode({ id, data, selected }: NodeProps) { }); }, [id, nodeData.error, nodeData.outputItems, nodeData.outputText, parsed.error, parsed.items, updateNodeData]); + // Helper: reparse and update outputs atomically whenever any split setting changes. + // Reads fresh data from the Zustand store (not React Flow props) to avoid stale closures. + const updateSettingsAndReparse = useCallback( + (partialSettings: Partial>) => { + const freshNode = useWorkflowStore.getState().nodes.find((n) => n.id === id); + if (!freshNode) return; + const fresh = freshNode.data as ArrayNodeData; + const merged = { + splitMode: partialSettings.splitMode ?? fresh.splitMode, + delimiter: partialSettings.delimiter ?? fresh.delimiter, + regexPattern: partialSettings.regexPattern ?? fresh.regexPattern, + trimItems: partialSettings.trimItems ?? fresh.trimItems, + removeEmpty: partialSettings.removeEmpty ?? fresh.removeEmpty, + }; + const result = parseTextToArray(fresh.inputText, merged); + updateNodeData(id, { + ...partialSettings, + outputItems: result.items, + outputText: JSON.stringify(result.items), + error: result.error, + }); + }, + [id, updateNodeData] + ); + const handleBasicModeChange = useCallback( (e: React.ChangeEvent) => { - updateNodeData(id, { splitMode: e.target.value as ArrayNodeData["splitMode"] }); + updateSettingsAndReparse({ splitMode: e.target.value as ArrayNodeData["splitMode"] }); }, - [id, updateNodeData] + [updateSettingsAndReparse] ); const previewItems = parsed.items; @@ -203,8 +234,11 @@ export function ArrayNode({ id, data, selected }: NodeProps) { className="nodrag nopan absolute top-2 right-2 z-10 shrink-0 p-1 bg-[#1a1a1a] rounded-md text-neutral-400 hover:text-neutral-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" title="Auto-route to Prompts" > - - + + + + + @@ -227,7 +261,7 @@ export function ArrayNode({ id, data, selected }: NodeProps) { @@ -109,7 +109,7 @@ export function ImageInputNode({ id, data, selected }: NodeProps {nodeData.image ? ( -
+
{nodeData.filename From e0381a5f2b4cc8f35184294c5a594405580a4cbe Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 22 Mar 2026 22:04:19 +1300 Subject: [PATCH 08/11] fix: add z-index to handles so they paint above positioned node content Nodes with `position: relative` on inner wrappers (e.g. image wrapper needing it for its remove button) caused left-side handles to render behind the wrapper due to CSS painting order. Adding z-index: 5 to all handles ensures they always paint above node content. Co-Authored-By: Claude Opus 4.6 --- src/app/globals.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/globals.css b/src/app/globals.css index f9b1a6fb..0e6c4a21 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -79,6 +79,7 @@ body { border-radius: 50%; border: 2px solid white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + z-index: 5; } /* Larger invisible hit area for easier clicking, especially when zoomed out */ From eabc607883af4dcf1201410e84884d3ddd1083cf Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 22 Mar 2026 22:17:21 +1300 Subject: [PATCH 09/11] fix: move ImageInputNode handles after visual content to prevent z-order clipping The left handle was rendered before the image wrapper div in DOM order. Since the image wrapper has position: relative (needed for its remove button), it painted over the handle. Moving both handles to render after visual content ensures they paint last (on top). Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/ImageInputNode.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/nodes/ImageInputNode.tsx b/src/components/nodes/ImageInputNode.tsx index 403708b8..3ae43d91 100644 --- a/src/components/nodes/ImageInputNode.tsx +++ b/src/components/nodes/ImageInputNode.tsx @@ -91,15 +91,6 @@ export function ImageInputNode({ id, data, selected }: NodeProps - {/* Reference input handle for visual links from Split Grid node */} - - )} + {/* Handles rendered after visual content so they paint on top */} + Date: Sun, 22 Mar 2026 22:27:56 +1300 Subject: [PATCH 10/11] fix: clamp expand height to minHeight and resolve text through switch nodes BaseNode: the settings expand branch now clamps the computed height to minHeight, matching the collapse and ResizeObserver branches. connectedInputs: resolveTextSourcesThroughRouters now recurses through switch nodes (same as routers) so upstream text sources are not missed. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/BaseNode.tsx | 3 ++- src/store/utils/connectedInputs.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index 59a8aed4..dbff515e 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -150,8 +150,9 @@ export function BaseNode({ : 0; const heightToAdd = finalHeight - savedPanelHeight; const currentHeight = getNodeDimension(node, "height"); + const newHeight = Math.max(minHeight, currentHeight + heightToAdd); return { - ...applyNodeDimensions(node, getNodeDimension(node, "width"), currentHeight + heightToAdd), + ...applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight), data: { ...node.data, _settingsPanelHeight: finalHeight }, }; }) diff --git a/src/store/utils/connectedInputs.ts b/src/store/utils/connectedInputs.ts index 2c8b817f..cf9c7821 100644 --- a/src/store/utils/connectedInputs.ts +++ b/src/store/utils/connectedInputs.ts @@ -138,7 +138,7 @@ export function resolveTextSourcesThroughRouters( if (seen.has(node.id)) continue; seen.add(node.id); - if (node.type === "router") { + if (node.type === "router" || node.type === "switch") { const upstreamNodes = edges .filter((e) => e.target === node.id && e.targetHandle === "text") .map((e) => allNodes.find((n) => n.id === e.source)) From cbd7c66355be8edeb535ebd62220c3aaa94577c4 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 22 Mar 2026 22:30:53 +1300 Subject: [PATCH 11/11] release: v1.1.3 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e850b17..424d08e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +## [1.1.3] - 2026-03-22 + +### Fixed + +- Clamp expand height to minHeight and resolve text through switch nodes +- Move ImageInputNode handles after visual content to prevent z-order clipping +- Add z-index to handles so they paint above positioned node content +- Move overflow-clip from contentClassName to inner visual wrappers to prevent handle clipping +- Move panel height correction from loadWorkflow into BaseNode render +- Prevent node height accumulation with inline parameters on reload +- Update WelcomeModal test to match bg-black/60 backdrop class +- Resolve prompt variables through router nodes for PromptConstructor +- Use overflow-visible on non-fullBleed nodes to prevent handle clipping + +### Other + +- Replace ArrayNode auto-route icon with Lucide split icon + ## [1.1.2] - 2026-03-12 ### Added diff --git a/package.json b/package.json index 63873cb9..a78eb875 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-banana", - "version": "1.1.2", + "version": "1.1.3", "private": true, "scripts": { "dev": "node server.js",