improve drag

This commit is contained in:
2026-03-07 11:04:07 +01:00
parent 59224880d2
commit 6572052dba
6 changed files with 145 additions and 61 deletions

View File

@@ -1,6 +1,5 @@
import type { ComponentProps, ReactNode } from "react";
import { NodeResizeControl } from "@xyflow/react";
import { Expand } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -11,20 +10,38 @@ export type BaseNodeProps = ComponentProps<"div"> & {
nodeId?: string;
/** Connection handles (InputHandle, OutputHandle). Rendered outside the overflow layer so they stay visible. */
handles?: ReactNode;
/** When provided, node uses this size (e.g. from React Flow width/height). Enables correct resize behavior. */
dimensions?: { width: number; height: number };
};
export function BaseNode({
className,
style,
dimensions,
resizable,
nodeId,
handles,
children,
...props
}: BaseNodeProps) {
const appliedStyle = style
? { ...style, display: "flex", flexDirection: "column" as const }
: undefined;
const hasSize =
dimensions &&
dimensions.width > 0 &&
dimensions.height > 0;
// Don't set overflow: hidden on the root — handles are positioned at left/right -10px
// and would be clipped. The inner content wrapper has overflow-hidden for scrolling.
const appliedStyle = hasSize
? {
...style,
width: dimensions.width,
height: dimensions.height,
display: "flex" as const,
flexDirection: "column" as const,
contain: "layout" as const,
}
: style
? { ...style, display: "flex", flexDirection: "column" as const }
: undefined;
return (
<div
@@ -44,28 +61,31 @@ export function BaseNode({
tabIndex={0}
{...props}
>
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">{children}</div>
<div
className="min-h-0 flex-1 flex flex-col overflow-hidden"
style={{ contain: "layout" }}
>
{children}
</div>
{resizable && nodeId && (
<div
className="flex shrink-0 items-center justify-end border-t border-border bg-muted/30 px-2 py-1.5"
data-slot="base-node-resize-footer"
<NodeResizeControl
nodeId={nodeId}
position="bottom-right"
minWidth={120}
minHeight={80}
className={cn(
"!border-0 !bg-transparent !rounded-none !p-0",
"absolute bottom-0 right-0 w-8 h-8 cursor-se-resize select-none",
"nodrag nopan touch-none",
)}
style={{ margin: 0, touchAction: "none", userSelect: "none" }}
>
<NodeResizeControl
nodeId={nodeId}
position="bottom-right"
minWidth={120}
minHeight={80}
className="!border-0 !bg-transparent !rounded-none"
style={{ padding: 0 }}
>
<span
className="flex size-6 items-center justify-center rounded border border-border bg-muted/80 text-muted-foreground shadow-sm nodrag nopan cursor-se-resize hover:bg-muted"
title="Drag to resize"
>
<Expand className="size-3.5" />
</span>
</NodeResizeControl>
</div>
<span
className="absolute bottom-0 right-0 w-8 h-8 rounded-br border-l border-t border-border/30 hover:border-border/50"
title="Drag corner to resize"
aria-label="Resize node"
/>
</NodeResizeControl>
)}
{handles}
</div>
@@ -110,6 +130,53 @@ export function BaseNodeHeaderTitle({
);
}
/**
* Standard node header: icon on the left, title taking the rest. Use for all node types.
* Inlined so it does not depend on BaseNodeHeader/BaseNodeHeaderTitle (avoids reference errors with HMR).
*/
export function BaseNodeHeaderRow({
icon,
title,
className,
}: {
icon: ReactNode;
title: ReactNode;
className?: string;
}) {
return (
<header
className={cn(
"shrink-0 mx-0 my-0 -mb-1 flex flex-row items-center justify-between gap-2 border-b px-3 py-2",
className,
)}
>
{icon}
<h3
data-slot="base-node-title"
className="user-select-none flex-1 font-semibold"
>
{title}
</h3>
</header>
);
}
/**
* Single-line muted text for the footer. Use for status or hints in all node types.
*/
export function BaseNodeFooterText({
className,
...props
}: ComponentProps<"div">) {
return (
<div
className={cn("w-full text-xs text-muted-foreground", className)}
data-slot="base-node-footer-text"
{...props}
/>
);
}
export function BaseNodeContent({
className,
...props