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); };