From a11634070fb5c805a1c398814a2d1c9b6bd95ad9 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 22 Mar 2026 08:15:45 -0500 Subject: [PATCH] Fix module drag drop (collision detection) + widen rack to fix port clipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collision detection root cause: pointerWithin returns ALL droppables containing the pointer, in registration order — not sorted by element size. Rack columns register via useSortable before their child RackSlots, so they always came first in the result list. over.data.current was { dragType: 'rack' }, never { dropType: 'slot' }, so handleDragEnd's slot check never matched and the module snapped back. Fix: filter droppableContainers to elements with data.current.dropType === 'slot' before running pointerWithin. This does an exact pointer hit-test against only the 44px slot rects. If no slot is hit (e.g. the pointer is in a gap or over a rack header), fall back to closestCenter over all droppables so rack-column reorder still works. Width fix: 24 ports * 10px + 23 gaps * 3px = 309px + px-2 padding (16px) + border-l-4 (4px) = 329px minimum w-80 (320px) was 9px short, clipping port 24. Increased to w-96 (384px) / min-w-[384px] — 55px of breathing room. Co-Authored-By: Claude Sonnet 4.6 --- client/src/components/rack/RackColumn.tsx | 2 +- client/src/components/rack/RackPlanner.tsx | 31 +++++++++++++++------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/client/src/components/rack/RackColumn.tsx b/client/src/components/rack/RackColumn.tsx index 58c3ccf..316f8c2 100644 --- a/client/src/components/rack/RackColumn.tsx +++ b/client/src/components/rack/RackColumn.tsx @@ -51,7 +51,7 @@ export function RackColumn({ rack, draggingModuleId }: RackColumnProps) { return ( <> -
+
{/* Rack header — drag handle for reorder */}
{/* Drag handle */} diff --git a/client/src/components/rack/RackPlanner.tsx b/client/src/components/rack/RackPlanner.tsx index 0232b45..119738f 100644 --- a/client/src/components/rack/RackPlanner.tsx +++ b/client/src/components/rack/RackPlanner.tsx @@ -55,18 +55,31 @@ function ModuleDragOverlay({ label }: { label: string }) { /** * Collision detection strategy: - * - pointerWithin first: finds droppables whose rect contains the pointer. - * This correctly hits the 44px-tall RackSlots since the pointer is inside them. - * - closestCenter fallback: used when the pointer is not within any droppable, - * which is when dragging a rack header between columns (sortable reorder). * - * Without this, the default closestCenter alone would favour the large rack-column - * sortable elements (~1800px tall) over the tiny slot droppables, so over.data - * would be { dragType: 'rack' } instead of { dropType: 'slot' }. + * Problem: SortableContext registers each rack column as a droppable. Columns + * are ~1800px tall; individual slots are 44px. Default closestCenter picks the + * column centre over any slot centre. pointerWithin returns multiple matches + * sorted by registration order (columns register before their child slots), so + * the rack column still wins. + * + * Fix: + * 1. First try pointerWithin restricted to ONLY elements whose data has + * dropType === 'slot'. This is an exact hit-test against the 44px slot rects. + * 2. Fall back to closestCenter over ALL droppables so rack-header reorder + * (which needs the sortable rack targets) still works. */ const slotFirstCollision: CollisionDetection = (args) => { - const within = pointerWithin(args); - if (within.length > 0) return within; + // Restrict to slot droppables only + const slotContainers = args.droppableContainers.filter( + (c) => c.data.current?.dropType === 'slot' + ); + + if (slotContainers.length > 0) { + const slotHits = pointerWithin({ ...args, droppableContainers: slotContainers }); + if (slotHits.length > 0) return slotHits; + } + + // Nothing hit a slot — use full closestCenter for rack reorder return closestCenter(args); };