Two root-cause bugs fixed:
1. Port <button> elements inside ModuleBlock had pointer-events:auto (browser
default), so document.elementFromPoint() hit them instead of the RackSlot
behind them whenever the cursor was over an occupied slot. Fixed by toggling
body.rack-dragging during any drag, which applies a CSS rule that forces
pointer-events:none !important on .module-block and all descendants.
2. onDragMove pointer-position reconstruction (activatorEvent.clientX + delta.x)
was slightly off because delta is measured from the initial mousedown, not
the activation point. Replaced with a native window pointermove listener
(capture phase) that gives exact clientX/Y — no reconstruction needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completely removes dnd-kit's useDroppable and collision detection for rack
slot targeting. Uses onDragMove + document.elementFromPoint() with data-rack-id
/ data-u-pos HTML attributes on RackSlot elements to resolve the hovered slot
independently of dnd-kit's SortableContext interference. Adds pointer-events-none
to ModuleBlock when isDragging so the invisible element doesn't block hit testing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
droppableContainers in @dnd-kit/core collision detection args is a custom
NodeMap class, not a plain Array. It implements [Symbol.iterator] (so
for...of works internally in closestCenter/pointerWithin) but does NOT
have Array.prototype methods like .filter().
Calling args.droppableContainers.filter(...) threw:
TypeError: args.droppableContainers.filter is not a function
dnd-kit silently catches errors in the collision detection callback and
treats them as no collision (over = null). Every module drag ended with
over = null, hitting the early return in handleDragEnd, causing the module
to snap back to its original slot every time.
Fix: Array.from(args.droppableContainers) converts the NodeMap iterable
to a plain array before filtering for dropType === 'slot' containers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Root cause: SortableContext registers each rack column as a droppable.
Each column is ~1800px tall (42U x 44px). The default closestCenter
algorithm compared center-to-center distances, so the rack column's
center consistently beat the 44px RackSlot's center — meaning over.data
resolved to { dragType: 'rack' } and handleDragEnd's check for
dropType === 'slot' never matched. Drops silently did nothing.
Fix: replace closestCenter with a two-phase collision detection:
1. pointerWithin — returns droppables whose bounding rect contains
the actual pointer position. Slots are exactly hit-tested.
2. closestCenter fallback — used when the pointer is not within any
registered droppable (e.g. dragging a rack header between columns
for sortable reorder where the pointer may be in the gap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete project scaffold with working auth, REST API, Prisma/SQLite
schema, Docker config, and React frontend for both Rack Planner and
Service Mapper modules. Both server and client pass TypeScript strict
mode with zero errors. Initial migration applied.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>