Fix module drag drop (collision detection) + widen rack to fix port clipping

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 08:15:45 -05:00
parent 172896b85f
commit a11634070f
2 changed files with 23 additions and 10 deletions

View File

@@ -51,7 +51,7 @@ export function RackColumn({ rack, draggingModuleId }: RackColumnProps) {
return ( return (
<> <>
<div ref={setNodeRef} style={style} className="flex flex-col min-w-[320px] w-80 shrink-0"> <div ref={setNodeRef} style={style} className="flex flex-col min-w-[384px] w-96 shrink-0">
{/* Rack header — drag handle for reorder */} {/* Rack header — drag handle for reorder */}
<div className="flex items-center gap-1 bg-slate-700 border border-slate-600 rounded-t-lg px-2 py-1.5 group"> <div className="flex items-center gap-1 bg-slate-700 border border-slate-600 rounded-t-lg px-2 py-1.5 group">
{/* Drag handle */} {/* Drag handle */}

View File

@@ -55,18 +55,31 @@ function ModuleDragOverlay({ label }: { label: string }) {
/** /**
* Collision detection strategy: * 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 * Problem: SortableContext registers each rack column as a droppable. Columns
* sortable elements (~1800px tall) over the tiny slot droppables, so over.data * are ~1800px tall; individual slots are 44px. Default closestCenter picks the
* would be { dragType: 'rack' } instead of { dropType: 'slot' }. * 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 slotFirstCollision: CollisionDetection = (args) => {
const within = pointerWithin(args); // Restrict to slot droppables only
if (within.length > 0) return within; 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); return closestCenter(args);
}; };