From 1e6aaca6295b3ee0fd2ee17f1525a646ad623eac Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 7 Mar 2026 21:59:50 +1300 Subject: [PATCH] fix: resolve aspect-fit failing after manual resize by aligning with React Flow dimension priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React Flow prioritizes node.width over node.style.width for rendering. NodeResizer writes to node.width but the aspect-fit handler was reading from node.style and only writing back to node.style — so resized dimensions were silently overridden. Extract getNodeDimension() and applyNodeDimensions() helpers to read/write both sources consistently. Also removes an unused Zustand store subscription that caused unnecessary re-renders. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/BaseNode.tsx | 94 +++++++++++++++---------------- 1 file changed, 45 insertions(+), 49 deletions(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index 04236610..4160d480 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -1,10 +1,12 @@ "use client"; import { ReactNode, useCallback } from "react"; -import { NodeResizer, OnResize, useReactFlow } from "@xyflow/react"; +import { Node, NodeResizer, OnResize, useReactFlow } from "@xyflow/react"; import { useWorkflowStore } from "@/store/workflowStore"; import { getMediaDimensions, calculateAspectFitSize } from "@/utils/nodeDimensions"; +const DEFAULT_NODE_DIMENSION = 300; + interface BaseNodeProps { id: string; children: ReactNode; @@ -21,6 +23,33 @@ interface BaseNodeProps { aspectFitMedia?: string | null; } +/** + * Read a node's effective width or height, respecting React Flow's internal + * priority: node.width > node.style.width > node.measured.width. + */ +function getNodeDimension(node: Node, axis: "width" | "height"): number { + return ( + (node[axis] as number) ?? + (node.style?.[axis] as number) ?? + (node.measured?.[axis] as number) ?? + DEFAULT_NODE_DIMENSION + ); +} + +/** + * Apply dimensions to a React Flow node, writing to both `node.width/height` + * (where NodeResizer writes) and `node.style` (the original source) so neither + * silently overrides the other. + */ +function applyNodeDimensions(node: Node, width: number, height: number): Node { + return { + ...node, + width, + height, + style: { ...node.style, width, height }, + }; +} + export function BaseNode({ id, children, @@ -35,41 +64,24 @@ export function BaseNode({ aspectFitMedia, }: BaseNodeProps) { const currentNodeIds = useWorkflowStore((state) => state.currentNodeIds); - const nodes = useWorkflowStore((state) => state.nodes); const setHoveredNodeId = useWorkflowStore((state) => state.setHoveredNodeId); const isCurrentlyExecuting = currentNodeIds.includes(id); const { getNodes, setNodes } = useReactFlow(); - // Synchronize resize across all selected nodes const handleResize: OnResize = useCallback( - (event, params) => { - const allNodes = getNodes(); - const selectedNodes = allNodes.filter((node) => node.selected && node.id !== id); - - if (selectedNodes.length > 0) { - setNodes((nodes) => - nodes.map((node) => { - if (node.selected && node.id !== id) { - return { - ...node, - style: { - ...node.style, - width: params.width, - height: params.height, - }, - }; - } - return node; - }) - ); - } + (_event, params) => { + setNodes((nodes) => + nodes.map((node) => { + if (node.selected && node.id !== id) { + return applyNodeDimensions(node, params.width, params.height); + } + return node; + }) + ); }, - [id, getNodes, setNodes] + [id, setNodes] ); - // Double-click resize handle → aspect-fit to media - // Uses React synthetic event on a `display: contents` wrapper so it stays - // in sync across React Flow re-renders (imperative DOM listeners break after manual resize). const handleResizeHandleDblClick = useCallback( async (e: React.MouseEvent) => { const target = e.target as HTMLElement; @@ -80,36 +92,20 @@ export function BaseNode({ const dims = await getMediaDimensions(aspectFitMedia); if (!dims) return; - const aspectRatio = dims.width / dims.height; - const allNodes = getNodes(); - const thisNode = allNodes.find((n) => n.id === id); + const thisNode = getNodes().find((n) => n.id === id); if (!thisNode) return; - const currentWidth = - (thisNode.style?.width as number) ?? - (thisNode.measured?.width as number) ?? - thisNode.width ?? - 300; - const currentHeight = - (thisNode.style?.height as number) ?? - (thisNode.measured?.height as number) ?? - thisNode.height ?? - 300; - const newSize = calculateAspectFitSize( - aspectRatio, - currentWidth, - currentHeight, + dims.width / dims.height, + getNodeDimension(thisNode, "width"), + getNodeDimension(thisNode, "height"), fullBleed ); setNodes((nds) => nds.map((n) => { if (n.id === id || (n.selected && n.id !== id)) { - return { - ...n, - style: { ...n.style, width: newSize.width, height: newSize.height }, - }; + return applyNodeDimensions(n, newSize.width, newSize.height); } return n; })