fix: node resize

This commit is contained in:
2026-03-11 00:38:15 +01:00
parent 2873f13875
commit c330713fa7
2 changed files with 44 additions and 19 deletions

View File

@@ -1,10 +1,14 @@
import type { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "react";
import { NodeResizeControl } from "@xyflow/react"; import { NodeResizer } from "@xyflow/react";
import { cn } from "@/lib/utils"; 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"> & { 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; resizable?: boolean;
/** Node id for the resize control (required when resizable is true). */ /** Node id for the resize control (required when resizable is true). */
nodeId?: string; nodeId?: string;
@@ -14,6 +18,8 @@ export type BaseNodeProps = ComponentProps<"div"> & {
dimensions?: { width: number; height: number }; dimensions?: { width: number; height: number };
/** When true, node is selected (from React Flow NodeProps). Used for visible selected state. */ /** When true, node is selected (from React Flow NodeProps). Used for visible selected state. */
selected?: boolean; 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({ export function BaseNode({
@@ -25,6 +31,7 @@ export function BaseNode({
handles, handles,
children, children,
selected, selected,
resizeConstraints,
...props ...props
}: BaseNodeProps) { }: BaseNodeProps) {
const hasSize = const hasSize =
@@ -46,6 +53,9 @@ export function BaseNode({
? { ...style, display: "flex", flexDirection: "column" as const } ? { ...style, display: "flex", flexDirection: "column" as const }
: undefined; : undefined;
const minW = resizeConstraints?.minWidth ?? RESIZE_MIN_WIDTH;
const minH = resizeConstraints?.minHeight ?? RESIZE_MIN_HEIGHT;
return ( return (
<div <div
className={cn( className={cn(
@@ -66,19 +76,16 @@ export function BaseNode({
{children} {children}
</div> </div>
{resizable && nodeId && ( {resizable && nodeId && (
<NodeResizeControl <NodeResizer
nodeId={nodeId} nodeId={nodeId}
position="bottom-right" isVisible={selected}
minWidth={120} minWidth={minW}
minHeight={80} minHeight={minH}
className={cn( maxWidth={resizeConstraints?.maxWidth}
"!border-0 !bg-transparent !rounded-none !p-0", maxHeight={resizeConstraints?.maxHeight}
"absolute bottom-0 right-0 w-8 h-8 cursor-se-resize select-none", color="transparent"
"nodrag nopan touch-none", handleClassName="base-node-resize-handle nodrag nopan"
)} />
style={{ margin: 0, touchAction: "none", userSelect: "none" }}
>
</NodeResizeControl>
)} )}
{handles} {handles}
</div> </div>

View File

@@ -142,18 +142,36 @@ body {
} }
} }
/* Resize control: snappy drag, no transition or selection delay */ /* NodeResizer: snappy drag; no border/background on handles; keep resize cursors. */
.react-flow__resize-control.bottom.right { .react-flow__resize-control {
cursor: se-resize;
touch-action: none; touch-action: none;
user-select: none; user-select: none;
} }
.react-flow__resize-control.bottom.right, .react-flow__resize-control,
.react-flow__resize-control.bottom.right * { .react-flow__resize-control * {
transition: none; transition: none;
} }
/* Subtle resize handles so they dont 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 */ /* Small helper for monospace pre output */
pre { pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;