From 510d478be33fffae31a43bbc95404f0532ee877d Mon Sep 17 00:00:00 2001 From: clawdbot Date: Thu, 29 Jan 2026 10:38:52 +0800 Subject: [PATCH] editor: group resize handles for multi-select --- packages/editor/src/editor/Canvas.tsx | 62 ++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/editor/Canvas.tsx b/packages/editor/src/editor/Canvas.tsx index 70c576d..a8ef8c7 100644 --- a/packages/editor/src/editor/Canvas.tsx +++ b/packages/editor/src/editor/Canvas.tsx @@ -48,6 +48,25 @@ export function Canvas(props: CanvasProps) { const bounds = useMemo(() => ({ w: props.screen.width, h: props.screen.height }), [props.screen.width, props.screen.height]); + const resizableSelected = useMemo(() => { + return props.screen.nodes.filter( + (n) => props.selectionIds.includes(n.id) && !n.locked && !n.hidden, + ); + }, [props.screen.nodes, props.selectionIds]); + + const groupResize = useMemo(() => { + if (resizableSelected.length < 2) return null; + const rects = resizableSelected.map((n) => n.rect); + const x1 = Math.min(...rects.map((r) => r.x)); + const y1 = Math.min(...rects.map((r) => r.y)); + const x2 = Math.max(...rects.map((r) => r.x + r.w)); + const y2 = Math.max(...rects.map((r) => r.y + r.h)); + return { + targetId: resizableSelected[0]!.id, + rect: { x: x1, y: y1, w: x2 - x1, h: y2 - y1 }, + }; + }, [resizableSelected]); + const clientToCanvas = useCallback((clientX: number, clientY: number) => { const el = ref.current; if (!el) return null; @@ -325,6 +344,7 @@ export function Canvas(props: CanvasProps) { key={node.id} node={node} selected={props.selectionIds.includes(node.id)} + selectionCount={props.selectionIds.length} onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); @@ -368,6 +388,17 @@ export function Canvas(props: CanvasProps) { /> ))} + {groupResize ? ( + { + e.preventDefault(); + e.stopPropagation(); + props.onBeginResize(e, groupResize.targetId, handle); + }} + /> + ) : null} + {box && (
void; onContextMenu: (e: React.MouseEvent) => void; onResizePointerDown: (e: React.PointerEvent, handle: ResizeHandle) => void; @@ -565,7 +597,35 @@ function NodeView(props: { } })()} - {props.selected && !node.locked && !node.hidden && } + {props.selected && + props.selectionCount === 1 && + !node.locked && + !node.hidden && } +
+ ); +} + +function GroupSelectionResizeBox(props: { + rect: { x: number; y: number; w: number; h: number }; + onPointerDown: (e: React.PointerEvent, handle: ResizeHandle) => void; +}) { + const r = props.rect; + return ( +
+
+ +
); }