From d6b23fabc55ca9c05b5c0ce3dc886e97df28b6f2 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Mon, 9 Mar 2026 23:01:25 +1300 Subject: [PATCH] fix: allow node resize from settings panel area Switch BaseNode wrapper from `display: contents` to flex layout when settings are expanded so NodeResizer handles span the full visual height. Height management is now centralized in BaseNode via useLayoutEffect + ResizeObserver instead of per-node useEffects that compounded height. - BaseNode: track settings panel height, adjust node dimensions on expand/collapse, observe dynamic content changes - InlineParameterPanel: remove animation (instant show/hide), remove selection ring logic (now handled by BaseNode outer container) - GenerateImageNode/GenerateVideoNode: remove buggy height management useEffects and parameterPanelRef - All 5 node types: remove `selected` prop from InlineParameterPanel Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/BaseNode.tsx | 99 ++++++++++++++++--- src/components/nodes/Generate3DNode.tsx | 1 - src/components/nodes/GenerateAudioNode.tsx | 1 - src/components/nodes/GenerateImageNode.tsx | 35 ------- src/components/nodes/GenerateVideoNode.tsx | 35 ------- src/components/nodes/InlineParameterPanel.tsx | 37 +++---- src/components/nodes/LLMGenerateNode.tsx | 1 - 7 files changed, 98 insertions(+), 111 deletions(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index af8abd49..cc1138ba 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -1,6 +1,6 @@ "use client"; -import { ReactNode, useCallback } from "react"; +import { ReactNode, useCallback, useRef, useLayoutEffect, useState } from "react"; import { Node, NodeResizer, OnResize, useReactFlow } from "@xyflow/react"; import { useWorkflowStore } from "@/store/workflowStore"; import { getMediaDimensions, calculateAspectFitSize } from "@/utils/nodeDimensions"; @@ -74,6 +74,78 @@ export function BaseNode({ const isCurrentlyExecuting = currentNodeIds.includes(id); const { getNodes, setNodes } = useReactFlow(); + const settingsPanelRef = useRef(null); + const [trackedSettingsHeight, setTrackedSettingsHeight] = useState(0); + + // Adjust node height when settings expand/collapse + useLayoutEffect(() => { + if (settingsExpanded && settingsPanel) { + // Expanding: measure settings panel and increase node height + const measure = () => { + const panelEl = settingsPanelRef.current; + if (!panelEl) return; + const panelHeight = panelEl.scrollHeight; + if (panelHeight === 0) return; + + setTrackedSettingsHeight(panelHeight); + setNodes((nodes) => + nodes.map((node) => { + if (node.id !== id) return node; + const currentHeight = getNodeDimension(node, "height"); + const newHeight = currentHeight + panelHeight; + return applyNodeDimensions(node, getNodeDimension(node, "width"), newHeight); + }) + ); + }; + + // Use rAF to ensure DOM has rendered the panel content + requestAnimationFrame(measure); + } else if (!settingsExpanded && trackedSettingsHeight > 0) { + // Collapsing: decrease node height by the tracked amount + const heightToRemove = trackedSettingsHeight; + setTrackedSettingsHeight(0); + setNodes((nodes) => + nodes.map((node) => { + 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); + }) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settingsExpanded]); + + // ResizeObserver to track dynamic settings panel height changes (e.g., model param count changes) + useLayoutEffect(() => { + if (!settingsExpanded || !settingsPanel) return; + const panelEl = settingsPanelRef.current; + if (!panelEl) return; + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const newPanelHeight = entry.contentRect.height; + if (newPanelHeight === 0) continue; + const delta = newPanelHeight - trackedSettingsHeight; + if (Math.abs(delta) < 2) continue; // Ignore sub-pixel changes + + setTrackedSettingsHeight(newPanelHeight); + setNodes((nodes) => + nodes.map((node) => { + 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); + }) + ); + } + }); + + observer.observe(panelEl); + return () => observer.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settingsExpanded, settingsPanel, trackedSettingsHeight]); + const handleResize: OnResize = useCallback( (_event, params) => { setNodes((nodes) => @@ -120,8 +192,15 @@ export function BaseNode({ [aspectFitMedia, id, fullBleed, getNodes, setNodes] ); + const hasExpandedSettings = settingsExpanded && settingsPanel; + return ( -
+
setHoveredNodeId(id)} onMouseLeave={() => setHoveredNodeId(null)} > - {/* When settings panel is expanded, render the selection ring as a separate clipped overlay - so it doesn't show a line at the bottom — the InlineParameterPanel draws the bottom half */} - {settingsExpanded && selected && ( -
- )}
{children}
- {settingsPanel} + {settingsPanel && ( +
+ {settingsPanel} +
+ )}
); } diff --git a/src/components/nodes/Generate3DNode.tsx b/src/components/nodes/Generate3DNode.tsx index 8cba4a51..99dfa12a 100644 --- a/src/components/nodes/Generate3DNode.tsx +++ b/src/components/nodes/Generate3DNode.tsx @@ -129,7 +129,6 @@ export function Generate3DNode({ id, data, selected }: NodeProps {/* Model selector: Browse button + current model display */}
diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx index b353766b..3d36e09d 100644 --- a/src/components/nodes/GenerateAudioNode.tsx +++ b/src/components/nodes/GenerateAudioNode.tsx @@ -242,7 +242,6 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps {/* Model selector: Browse button + current model display */}
diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index f8e922db..f90716c4 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -52,7 +52,6 @@ export function GenerateImageNode({ id, data, selected }: NodeProps(null); // Get the current selected provider (default to gemini) const currentProvider: ProviderType = nodeData.selectedModel?.provider || "gemini"; @@ -495,39 +494,6 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { - if (!inlineParametersEnabled) return; - - // Use RAF to wait for render - requestAnimationFrame(() => { - requestAnimationFrame(() => { - const currentNode = useWorkflowStore.getState().nodes.find(n => n.id === id); - if (!currentNode) return; - - const baseHeight = typeof currentNode.style?.height === 'number' - ? currentNode.style.height - : 300; - - let paramHeight = 0; - if (isParamsExpanded && parameterPanelRef.current) { - // Measure the actual rendered height of parameters - const panelContent = parameterPanelRef.current.querySelector('#params-' + id); - if (panelContent) { - paramHeight = (panelContent as HTMLElement).scrollHeight; - } - } - // Add chevron button height (~28px) - const chevronHeight = 28; - const totalHeight = baseHeight + chevronHeight + paramHeight; - - setNodes(nodes => nodes.map(n => - n.id === id ? { ...n, style: { ...n.style, height: totalHeight } } : n - )); - }); - }); - }, [id, isParamsExpanded, inlineParametersEnabled, setNodes]); - return ( <> {/* Gemini-specific controls */} {isGeminiProvider && currentModelId && ( diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index 1c4c70e4..3a8c7953 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -57,7 +57,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps(null); const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; @@ -403,39 +402,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { - if (!inlineParametersEnabled) return; - - // Use RAF to wait for render - requestAnimationFrame(() => { - requestAnimationFrame(() => { - const currentNode = useWorkflowStore.getState().nodes.find(n => n.id === id); - if (!currentNode) return; - - const baseHeight = typeof currentNode.style?.height === 'number' - ? currentNode.style.height - : 300; - - let paramHeight = 0; - if (isParamsExpanded && parameterPanelRef.current) { - // Measure the actual rendered height of parameters - const panelContent = parameterPanelRef.current.querySelector('#params-' + id); - if (panelContent) { - paramHeight = (panelContent as HTMLElement).scrollHeight; - } - } - // Add chevron button height (~28px) - const chevronHeight = 28; - const totalHeight = baseHeight + chevronHeight + paramHeight; - - setNodes(nodes => nodes.map(n => - n.id === id ? { ...n, style: { ...n.style, height: totalHeight } } : n - )); - }); - }); - }, [id, isParamsExpanded, inlineParametersEnabled, setNodes]); - return ( <> {/* External provider parameters - reuse ModelParameters component */} {nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && ( diff --git a/src/components/nodes/InlineParameterPanel.tsx b/src/components/nodes/InlineParameterPanel.tsx index 1f167612..d9f6238b 100644 --- a/src/components/nodes/InlineParameterPanel.tsx +++ b/src/components/nodes/InlineParameterPanel.tsx @@ -7,31 +7,20 @@ interface InlineParameterPanelProps { onToggle: () => void; children: ReactNode; nodeId: string; - selected?: boolean; } /** * Collapsible parameter container for inline display within generation nodes. - * Provides a chevron toggle button and smooth expand/collapse transitions. + * Provides a chevron toggle button and instant expand/collapse. */ export function InlineParameterPanel({ expanded, onToggle, children, nodeId, - selected = false, }: InlineParameterPanelProps) { - // When expanded + selected, draw ring on bottom half and clip the top edge - // so it merges seamlessly with BaseNode's ring (which clips its bottom edge) - const ringClass = expanded && selected - ? "ring-2 ring-blue-500/40 rounded-b-lg" - : ""; - const ringStyle = expanded && selected - ? { clipPath: 'inset(0 -20px -20px -20px)' } - : undefined; - return ( -
+ <> {/* Settings toggle button — no background when collapsed, floats below node edge */} - {/* Collapsible content area */} -
-
-
{children}
+ {/* Content area — instant show/hide */} + {expanded && ( +
+ {children}
-
-
+ )} + ); } diff --git a/src/components/nodes/LLMGenerateNode.tsx b/src/components/nodes/LLMGenerateNode.tsx index 03ae6924..f19ab45f 100644 --- a/src/components/nodes/LLMGenerateNode.tsx +++ b/src/components/nodes/LLMGenerateNode.tsx @@ -131,7 +131,6 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps {/* LLM-specific controls */}