diff --git a/frontend/src/components/base/BaseNode.tsx b/frontend/src/components/base/BaseNode.tsx
index d84e1a7..498e1a5 100644
--- a/frontend/src/components/base/BaseNode.tsx
+++ b/frontend/src/components/base/BaseNode.tsx
@@ -1,10 +1,14 @@
import type { ComponentProps, ReactNode } from "react";
-import { NodeResizeControl } from "@xyflow/react";
+import { NodeResizer } from "@xyflow/react";
import { cn } from "@/lib/utils";
+/** Default min size for resizable nodes (used by NodeResizer). */
+export const RESIZE_MIN_WIDTH = 120;
+export const RESIZE_MIN_HEIGHT = 80;
+
export type BaseNodeProps = ComponentProps<"div"> & {
- /** When true, shows a bottom-right resize handle. Requires nodeId when used inside React Flow. */
+ /** When true, shows resize handles on corners/edges (via NodeResizer). Requires nodeId when used inside React Flow. */
resizable?: boolean;
/** Node id for the resize control (required when resizable is true). */
nodeId?: string;
@@ -14,6 +18,8 @@ export type BaseNodeProps = ComponentProps<"div"> & {
dimensions?: { width: number; height: number };
/** When true, node is selected (from React Flow NodeProps). Used for visible selected state. */
selected?: boolean;
+ /** Optional min/max for NodeResizer. Omit to use RESIZE_MIN_WIDTH / RESIZE_MIN_HEIGHT. */
+ resizeConstraints?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };
};
export function BaseNode({
@@ -25,6 +31,7 @@ export function BaseNode({
handles,
children,
selected,
+ resizeConstraints,
...props
}: BaseNodeProps) {
const hasSize =
@@ -46,6 +53,9 @@ export function BaseNode({
? { ...style, display: "flex", flexDirection: "column" as const }
: undefined;
+ const minW = resizeConstraints?.minWidth ?? RESIZE_MIN_WIDTH;
+ const minH = resizeConstraints?.minHeight ?? RESIZE_MIN_HEIGHT;
+
return (
{resizable && nodeId && (
-
-
+ isVisible={selected}
+ minWidth={minW}
+ minHeight={minH}
+ maxWidth={resizeConstraints?.maxWidth}
+ maxHeight={resizeConstraints?.maxHeight}
+ color="transparent"
+ handleClassName="base-node-resize-handle nodrag nopan"
+ />
)}
{handles}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 780dc5f..3efd409 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -142,18 +142,36 @@ body {
}
}
-/* Resize control: snappy drag, no transition or selection delay */
-.react-flow__resize-control.bottom.right {
- cursor: se-resize;
+/* NodeResizer: snappy drag; no border/background on handles; keep resize cursors. */
+.react-flow__resize-control {
touch-action: none;
user-select: none;
}
-.react-flow__resize-control.bottom.right,
-.react-flow__resize-control.bottom.right * {
+.react-flow__resize-control,
+.react-flow__resize-control * {
transition: none;
}
+/* Subtle resize handles so they don’t compete with the selected-node ring (primary). Shown only when node is selected. */
+/* Single bottom-right resize handle: no border, transparent so only the corner grab area remains. */
+/* Handles: invisible (no border/background); cursor unchanged so resize cursors stay. */
+.base-node-resize-handle,
+.base-node-resize-handle * {
+ border: none !important;
+ background: transparent !important;
+}
+
+/* Resize cursor per position (keep cursor style change on hover). */
+.react-flow__resize-control.top.left { cursor: nw-resize; }
+.react-flow__resize-control.top.right { cursor: ne-resize; }
+.react-flow__resize-control.bottom.left { cursor: sw-resize; }
+.react-flow__resize-control.bottom.right { cursor: se-resize; }
+.react-flow__resize-control.top { cursor: n-resize; }
+.react-flow__resize-control.right { cursor: e-resize; }
+.react-flow__resize-control.bottom { cursor: s-resize; }
+.react-flow__resize-control.left { cursor: w-resize; }
+
/* Small helper for monospace pre output */
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;