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;