"use client"; import { ReactNode, useCallback } from "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; selected?: boolean; isExecuting?: boolean; hasError?: boolean; className?: string; contentClassName?: string; minWidth?: number; minHeight?: number; /** When true, node has no background/border — content fills the entire node area */ fullBleed?: boolean; /** Media URL (image/video) to use for aspect-fit resize on resize-handle double-click */ aspectFitMedia?: string | null; /** When true, bottom corners lose rounding so the selection ring connects to the settings panel below */ settingsExpanded?: boolean; } /** * 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, selected = false, isExecuting = false, hasError = false, className = "", contentClassName, minWidth = 180, minHeight = 100, fullBleed = false, aspectFitMedia, settingsExpanded = false, }: BaseNodeProps) { const currentNodeIds = useWorkflowStore((state) => state.currentNodeIds); const setHoveredNodeId = useWorkflowStore((state) => state.setHoveredNodeId); const isCurrentlyExecuting = currentNodeIds.includes(id); const { getNodes, setNodes } = useReactFlow(); const handleResize: OnResize = useCallback( (_event, params) => { setNodes((nodes) => nodes.map((node) => { if (node.selected && node.id !== id) { return applyNodeDimensions(node, params.width, params.height); } return node; }) ); }, [id, setNodes] ); const handleResizeHandleDblClick = useCallback( async (e: React.MouseEvent) => { const target = e.target as HTMLElement; if (!target.closest(".react-flow__resize-control")) return; if (!aspectFitMedia) return; e.stopPropagation(); const dims = await getMediaDimensions(aspectFitMedia); if (!dims) return; const thisNode = getNodes().find((n) => n.id === id); if (!thisNode) return; const newSize = calculateAspectFitSize( 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 applyNodeDimensions(n, newSize.width, newSize.height); } return n; }) ); }, [aspectFitMedia, id, fullBleed, getNodes, setNodes] ); return (
setHoveredNodeId(id)} onMouseLeave={() => setHoveredNodeId(null)} >
{children}
); }