@archie/devtools 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -581,44 +581,6 @@ function getFirstCollision(collisions, property) {
581
581
  const [firstCollision] = collisions;
582
582
  return property ? firstCollision[property] : firstCollision;
583
583
  }
584
- function centerOfRectangle(rect, left, top) {
585
- if (left === void 0) {
586
- left = rect.left;
587
- }
588
- if (top === void 0) {
589
- top = rect.top;
590
- }
591
- return {
592
- x: left + rect.width * 0.5,
593
- y: top + rect.height * 0.5
594
- };
595
- }
596
- var closestCenter = (_ref) => {
597
- let {
598
- collisionRect,
599
- droppableRects,
600
- droppableContainers
601
- } = _ref;
602
- const centerRect = centerOfRectangle(collisionRect, collisionRect.left, collisionRect.top);
603
- const collisions = [];
604
- for (const droppableContainer of droppableContainers) {
605
- const {
606
- id
607
- } = droppableContainer;
608
- const rect = droppableRects.get(id);
609
- if (rect) {
610
- const distBetween = distanceBetween(centerOfRectangle(rect), centerRect);
611
- collisions.push({
612
- id,
613
- data: {
614
- droppableContainer,
615
- value: distBetween
616
- }
617
- });
618
- }
619
- }
620
- return collisions.sort(sortCollisionsAsc);
621
- };
622
584
  var closestCorners = (_ref) => {
623
585
  let {
624
586
  collisionRect,
@@ -5547,6 +5509,7 @@ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
5547
5509
  var HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
5548
5510
  var HANDLE_HIT_SIZE = 24;
5549
5511
  var HANDLE_KNOB_SIZE = 8;
5512
+ var EMPTY_SCROLL_DELTA = { x: NaN, y: NaN };
5550
5513
  var OVERLAY_ROOT_STYLE = {
5551
5514
  position: "fixed",
5552
5515
  inset: 0,
@@ -5620,17 +5583,39 @@ function handlePosition(dir) {
5620
5583
  };
5621
5584
  return positions[dir];
5622
5585
  }
5623
- function getOutlineStyle(rect, isSelected, inspectorTheme) {
5586
+ function getSelectedOutlineStyle(rect, inspectorTheme) {
5624
5587
  return {
5625
5588
  ...OUTLINE_BASE_STYLE,
5626
5589
  left: rect.left,
5627
5590
  top: rect.top,
5628
5591
  width: rect.width,
5629
5592
  height: rect.height,
5630
- border: isSelected ? `2px solid ${inspectorTheme.accent}` : `1px dashed ${inspectorTheme.shared}`,
5631
- backgroundColor: isSelected ? inspectorTheme.accentSoft : "transparent"
5593
+ border: `2px solid ${inspectorTheme.accent}`,
5594
+ backgroundColor: inspectorTheme.accentSoft
5632
5595
  };
5633
5596
  }
5597
+ function getGuideOutlineStyle(rect, tier, inspectorTheme) {
5598
+ const base = {
5599
+ ...OUTLINE_BASE_STYLE,
5600
+ left: rect.left,
5601
+ top: rect.top,
5602
+ width: rect.width,
5603
+ height: rect.height
5604
+ };
5605
+ switch (tier) {
5606
+ case "candidate":
5607
+ return { ...base, border: `1px solid ${inspectorTheme.accent}`, opacity: 0.2 };
5608
+ case "lane-child":
5609
+ return { ...base, border: `1px dashed ${inspectorTheme.accent}`, opacity: 0.45 };
5610
+ case "lane":
5611
+ return {
5612
+ ...base,
5613
+ border: `1.5px solid ${inspectorTheme.accent}`,
5614
+ backgroundColor: inspectorTheme.accentSoft,
5615
+ opacity: 0.75
5616
+ };
5617
+ }
5618
+ }
5634
5619
  function getResizeHandleStyle(position) {
5635
5620
  return {
5636
5621
  ...RESIZE_HANDLE_BASE_STYLE,
@@ -5653,40 +5638,58 @@ function getLabelStyle(labelLeft, labelTop, labelColor) {
5653
5638
  backgroundColor: labelColor
5654
5639
  };
5655
5640
  }
5641
+ function projectDragRect(dragGuide, nodeId, sdx, sdy) {
5642
+ const r = dragGuide.rects.get(nodeId);
5643
+ if (!r) return null;
5644
+ return { left: r.left - sdx, top: r.top - sdy, width: r.width, height: r.height };
5645
+ }
5656
5646
  function ElementHighlight({
5657
- elements,
5658
- draggingNodeId,
5659
- activeEl,
5660
- arrangeSelectedNodeId,
5647
+ selected,
5648
+ dragGuide,
5661
5649
  externalDragActive,
5662
5650
  onResizeHandlePointerDown
5663
5651
  }) {
5664
5652
  const inspectorTheme = useDesignModeStore((s) => s.inspectorTheme);
5665
- const internalDragInProgress = draggingNodeId !== null;
5666
- const overlayVisible = arrangeSelectedNodeId !== null || internalDragInProgress;
5667
5653
  const containerRef = useRef5(null);
5668
5654
  const rafRef = useRef5(0);
5655
+ const lastScrollDeltaRef = useRef5(EMPTY_SCROLL_DELTA);
5669
5656
  const syncPositions = useCallback5(() => {
5670
5657
  const container = containerRef.current;
5671
5658
  if (!container) return;
5672
- const outlines = container.querySelectorAll("[data-outline-for]");
5673
- for (const outline of outlines) {
5674
- const nodeId = outline.getAttribute("data-outline-for");
5675
- const el = elements.find((e) => e.nodeId === nodeId);
5676
- if (!el) continue;
5677
- const rect = el.element.getBoundingClientRect();
5678
- outline.style.left = `${rect.left}px`;
5679
- outline.style.top = `${rect.top}px`;
5680
- outline.style.width = `${rect.width}px`;
5681
- outline.style.height = `${rect.height}px`;
5682
- const label = outline.nextElementSibling;
5683
- if (label) {
5684
- label.style.left = `${rect.left}px`;
5685
- label.style.top = `${rect.top}px`;
5659
+ if (dragGuide) {
5660
+ const sdx = window.scrollX - dragGuide.startScroll.x;
5661
+ const sdy = window.scrollY - dragGuide.startScroll.y;
5662
+ if (sdx === lastScrollDeltaRef.current.x && sdy === lastScrollDeltaRef.current.y) return;
5663
+ lastScrollDeltaRef.current = { x: sdx, y: sdy };
5664
+ const outlines = container.querySelectorAll("[data-guide-for]");
5665
+ for (const outline2 of outlines) {
5666
+ const nodeId = outline2.getAttribute("data-guide-for");
5667
+ const r = dragGuide.rects.get(nodeId);
5668
+ if (!r) continue;
5669
+ outline2.style.left = `${r.left - sdx}px`;
5670
+ outline2.style.top = `${r.top - sdy}px`;
5686
5671
  }
5672
+ return;
5687
5673
  }
5688
- }, [elements]);
5674
+ if (!selected) return;
5675
+ const rect2 = selected.element.getBoundingClientRect();
5676
+ const outline = container.querySelector("[data-outline-for]");
5677
+ if (outline) {
5678
+ outline.style.left = `${rect2.left}px`;
5679
+ outline.style.top = `${rect2.top}px`;
5680
+ outline.style.width = `${rect2.width}px`;
5681
+ outline.style.height = `${rect2.height}px`;
5682
+ }
5683
+ const label = container.querySelector("[data-drag-handle]");
5684
+ if (label) {
5685
+ label.style.left = `${rect2.left + 12}px`;
5686
+ label.style.top = `${Math.max(0, rect2.top - 20)}px`;
5687
+ }
5688
+ }, [dragGuide, selected]);
5689
+ const loopActive = !externalDragActive && (dragGuide !== null || selected !== null);
5689
5690
  useEffect5(() => {
5691
+ if (!loopActive) return;
5692
+ lastScrollDeltaRef.current = EMPTY_SCROLL_DELTA;
5690
5693
  let running = true;
5691
5694
  const loop = () => {
5692
5695
  if (!running) return;
@@ -5698,81 +5701,113 @@ function ElementHighlight({
5698
5701
  running = false;
5699
5702
  cancelAnimationFrame(rafRef.current);
5700
5703
  };
5701
- }, [syncPositions]);
5702
- if (!overlayVisible) return null;
5703
- if (externalDragActive) return null;
5704
- return /* @__PURE__ */ jsx2(
5705
- "div",
5706
- {
5707
- ref: containerRef,
5708
- "data-design-mode-ui": true,
5709
- style: OVERLAY_ROOT_STYLE,
5710
- children: elements.map((el) => {
5711
- if (el.nodeId === draggingNodeId) return null;
5712
- if (activeEl && activeEl !== el.element && activeEl.contains(el.element)) return null;
5713
- const isSelected = el.nodeId === arrangeSelectedNodeId;
5714
- if (!isSelected && !internalDragInProgress) return null;
5715
- const rect = el.element.getBoundingClientRect();
5716
- const showLabel = isSelected;
5717
- const labelTop = isSelected ? Math.max(0, rect.top - 20) : rect.top;
5718
- const labelLeft = isSelected ? rect.left + 12 : rect.left;
5719
- return /* @__PURE__ */ jsxs("div", { children: [
5720
- /* @__PURE__ */ jsx2(
5721
- "div",
5722
- {
5723
- "data-outline-for": el.nodeId,
5724
- "data-design-mode-ui": true,
5725
- style: getOutlineStyle(rect, isSelected, inspectorTheme),
5726
- children: isSelected && el.draggable && HANDLES.map((dir) => {
5727
- const pos = handlePosition(dir);
5728
- return /* @__PURE__ */ jsx2(
5729
- "div",
5730
- {
5731
- "data-design-mode-ui": true,
5732
- "data-resize-handle": dir,
5733
- onPointerDown: (e) => {
5734
- e.stopPropagation();
5735
- onResizeHandlePointerDown?.(e, el.nodeId, dir);
5736
- },
5737
- style: getResizeHandleStyle(pos),
5738
- children: /* @__PURE__ */ jsx2(
5739
- "span",
5740
- {
5741
- "data-design-mode-ui": true,
5742
- style: getResizeKnobStyle(inspectorTheme.accent)
5743
- }
5744
- )
5745
- },
5746
- dir
5747
- );
5748
- })
5749
- }
5750
- ),
5751
- showLabel && /* @__PURE__ */ jsxs(
5704
+ }, [loopActive, syncPositions]);
5705
+ if (!loopActive) return null;
5706
+ if (dragGuide) {
5707
+ const sdx = window.scrollX - dragGuide.startScroll.x;
5708
+ const sdy = window.scrollY - dragGuide.startScroll.y;
5709
+ const { candidateContainerIds, activeContainerId, activeChildIds, draggingNodeId } = dragGuide;
5710
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, "data-design-mode-ui": true, style: OVERLAY_ROOT_STYLE, children: [
5711
+ [...candidateContainerIds].map((id) => {
5712
+ if (id === activeContainerId || id === draggingNodeId) return null;
5713
+ const rect2 = projectDragRect(dragGuide, id, sdx, sdy);
5714
+ if (!rect2) return null;
5715
+ return /* @__PURE__ */ jsx2(
5716
+ "div",
5717
+ {
5718
+ "data-guide-for": id,
5719
+ "data-guide-tier": "candidate",
5720
+ "data-design-mode-ui": true,
5721
+ style: getGuideOutlineStyle(rect2, "candidate", inspectorTheme)
5722
+ },
5723
+ `candidate:${id}`
5724
+ );
5725
+ }),
5726
+ activeContainerId !== null && activeChildIds.map((id) => {
5727
+ if (id === draggingNodeId) return null;
5728
+ const rect2 = projectDragRect(dragGuide, id, sdx, sdy);
5729
+ if (!rect2) return null;
5730
+ return /* @__PURE__ */ jsx2(
5731
+ "div",
5732
+ {
5733
+ "data-guide-for": id,
5734
+ "data-guide-tier": "lane-child",
5735
+ "data-design-mode-ui": true,
5736
+ style: getGuideOutlineStyle(rect2, "lane-child", inspectorTheme)
5737
+ },
5738
+ `lane-child:${id}`
5739
+ );
5740
+ }),
5741
+ activeContainerId !== null && activeContainerId !== draggingNodeId && (() => {
5742
+ const rect2 = projectDragRect(dragGuide, activeContainerId, sdx, sdy);
5743
+ if (!rect2) return null;
5744
+ return /* @__PURE__ */ jsx2(
5745
+ "div",
5746
+ {
5747
+ "data-guide-for": activeContainerId,
5748
+ "data-guide-tier": "lane",
5749
+ "data-design-mode-ui": true,
5750
+ style: getGuideOutlineStyle(rect2, "lane", inspectorTheme)
5751
+ },
5752
+ `lane:${activeContainerId}`
5753
+ );
5754
+ })()
5755
+ ] });
5756
+ }
5757
+ if (!selected) return null;
5758
+ const rect = selected.element.getBoundingClientRect();
5759
+ const labelTop = Math.max(0, rect.top - 20);
5760
+ const labelLeft = rect.left + 12;
5761
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, "data-design-mode-ui": true, style: OVERLAY_ROOT_STYLE, children: [
5762
+ /* @__PURE__ */ jsx2(
5763
+ "div",
5764
+ {
5765
+ "data-outline-for": selected.nodeId,
5766
+ "data-design-mode-ui": true,
5767
+ style: getSelectedOutlineStyle(rect, inspectorTheme),
5768
+ children: selected.draggable && HANDLES.map((dir) => {
5769
+ const pos = handlePosition(dir);
5770
+ return /* @__PURE__ */ jsx2(
5752
5771
  "div",
5753
5772
  {
5754
5773
  "data-design-mode-ui": true,
5755
- "data-drag-handle": el.nodeId,
5756
- style: getLabelStyle(
5757
- labelLeft,
5758
- labelTop,
5759
- isSelected ? inspectorTheme.accent : inspectorTheme.hover
5760
- ),
5761
- children: [
5762
- /* @__PURE__ */ jsx2("span", { style: LABEL_ICON_STYLE, children: "\u283F" }),
5763
- el.label,
5764
- isSelected && /* @__PURE__ */ jsxs("span", { style: LABEL_DIMENSIONS_STYLE, children: [
5765
- Math.round(rect.width),
5766
- "\xD7",
5767
- Math.round(rect.height)
5768
- ] })
5769
- ]
5770
- }
5771
- )
5772
- ] }, el.nodeId);
5773
- })
5774
- }
5775
- );
5774
+ "data-resize-handle": dir,
5775
+ onPointerDown: (e) => {
5776
+ e.stopPropagation();
5777
+ onResizeHandlePointerDown?.(e, selected.nodeId, dir);
5778
+ },
5779
+ style: getResizeHandleStyle(pos),
5780
+ children: /* @__PURE__ */ jsx2(
5781
+ "span",
5782
+ {
5783
+ "data-design-mode-ui": true,
5784
+ style: getResizeKnobStyle(inspectorTheme.accent)
5785
+ }
5786
+ )
5787
+ },
5788
+ dir
5789
+ );
5790
+ })
5791
+ }
5792
+ ),
5793
+ /* @__PURE__ */ jsxs(
5794
+ "div",
5795
+ {
5796
+ "data-design-mode-ui": true,
5797
+ "data-drag-handle": selected.nodeId,
5798
+ style: getLabelStyle(labelLeft, labelTop, inspectorTheme.accent),
5799
+ children: [
5800
+ /* @__PURE__ */ jsx2("span", { style: LABEL_ICON_STYLE, children: "\u283F" }),
5801
+ selected.label,
5802
+ /* @__PURE__ */ jsxs("span", { style: LABEL_DIMENSIONS_STYLE, children: [
5803
+ Math.round(rect.width),
5804
+ "\xD7",
5805
+ Math.round(rect.height)
5806
+ ] })
5807
+ ]
5808
+ }
5809
+ )
5810
+ ] });
5776
5811
  }
5777
5812
 
5778
5813
  // src/client/dnd/design-mode/projection.ts
@@ -6566,30 +6601,6 @@ function applyDndCommandPlacement(command, direction, resolveNodeElement) {
6566
6601
  return true;
6567
6602
  }
6568
6603
 
6569
- // src/client/dnd/design-mode/design-mode-collision.ts
6570
- function filterDroppables(droppableContainers, blockedContainerIds) {
6571
- if (!blockedContainerIds || blockedContainerIds.size === 0) return droppableContainers;
6572
- return droppableContainers.filter((container) => !blockedContainerIds.has(String(container.id)));
6573
- }
6574
- function getCollisions(detector, args) {
6575
- return detector(args);
6576
- }
6577
- function createDesignModeCollisionDetection(blockedContainerIds) {
6578
- return (args) => {
6579
- const droppableContainers = filterDroppables(args.droppableContainers, blockedContainerIds);
6580
- if (droppableContainers.length === 0) return [];
6581
- const scopedArgs = {
6582
- ...args,
6583
- droppableContainers
6584
- };
6585
- const pointerHits = getCollisions(pointerWithin, scopedArgs);
6586
- if (pointerHits.length > 0) return pointerHits;
6587
- const cornerHits = getCollisions(closestCorners, scopedArgs);
6588
- if (cornerHits.length > 0) return cornerHits;
6589
- return getCollisions(closestCenter, scopedArgs);
6590
- };
6591
- }
6592
-
6593
6604
  // src/client/dnd/design-mode/arrange-session.ts
6594
6605
  function serializeArrangeChange(event, index) {
6595
6606
  return {
@@ -6868,9 +6879,9 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6868
6879
  const lastIntentRef = useRef8(null);
6869
6880
  const visualIntentRef = useRef8(null);
6870
6881
  const sourceContainerIdRef = useRef8(null);
6882
+ const activeLaneContainerIdRef = useRef8(null);
6871
6883
  const rowLockedRef = useRef8(false);
6872
6884
  const dragSessionAnchorRef = useRef8(null);
6873
- const lockRef = useRef8({ containerId: null, isCrossUnlocked: false, exitSince: null, unlockUntilTs: null });
6874
6885
  const targetPlaneRef = useRef8("lane");
6875
6886
  const nestCandidateRef = useRef8(null);
6876
6887
  const nestTargetRef = useRef8(null);
@@ -6880,7 +6891,6 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6880
6891
  const sourceCommitLaneRef = useRef8(null);
6881
6892
  const commitLaneCacheRef = useRef8(/* @__PURE__ */ new Map());
6882
6893
  const safeZoneRef = useRef8(null);
6883
- const blockedContainerIdsRef = useRef8(/* @__PURE__ */ new Set());
6884
6894
  const historyRef = useRef8(new DndCommandHistory());
6885
6895
  const processedUndoIdRef = useRef8(undoRequestId);
6886
6896
  const processedRedoIdRef = useRef8(redoRequestId);
@@ -6900,9 +6910,9 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6900
6910
  lastIntentRef,
6901
6911
  visualIntentRef,
6902
6912
  sourceContainerIdRef,
6913
+ activeLaneContainerIdRef,
6903
6914
  rowLockedRef,
6904
6915
  dragSessionAnchorRef,
6905
- lockRef,
6906
6916
  targetPlaneRef,
6907
6917
  nestCandidateRef,
6908
6918
  nestTargetRef,
@@ -6912,7 +6922,6 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6912
6922
  sourceCommitLaneRef,
6913
6923
  commitLaneCacheRef,
6914
6924
  safeZoneRef,
6915
- blockedContainerIdsRef,
6916
6925
  historyRef,
6917
6926
  processedUndoIdRef,
6918
6927
  processedRedoIdRef,
@@ -7180,6 +7189,11 @@ function pointInRect(px, py, r) {
7180
7189
  function pointInExpandedRect(px, py, r, inset) {
7181
7190
  return px >= r.left - inset && px <= r.right + inset && py >= r.top - inset && py <= r.bottom + inset;
7182
7191
  }
7192
+ function isInNestZone(px, py, rect) {
7193
+ const relX = (px - rect.left) / rect.width;
7194
+ const relY = (py - rect.top) / rect.height;
7195
+ return relX > NEST_EDGE_FRACTION && relX < 1 - NEST_EDGE_FRACTION && relY > NEST_EDGE_FRACTION && relY < 1 - NEST_EDGE_FRACTION;
7196
+ }
7183
7197
  function buildSlotQuadtree(slotIndex) {
7184
7198
  const rects = /* @__PURE__ */ new Map();
7185
7199
  for (const [id, slot] of slotIndex) {
@@ -7222,6 +7236,11 @@ var DragIntentEngine = class {
7222
7236
  __publicField(this, "laneAllowedContainers");
7223
7237
  __publicField(this, "blockedContainerIds");
7224
7238
  __publicField(this, "targetPlane", "lane");
7239
+ // Memoized result of getValidDropContainerIds(). Safe to cache because every
7240
+ // input it reads (containerIndex, slotIndex, activeId, laneAllowedContainers,
7241
+ // blockedContainerIds) is fixed in the constructor and never mutated during
7242
+ // a drag.
7243
+ __publicField(this, "validDropContainerIds", null);
7225
7244
  this.projectionTree = projection.projectionTree;
7226
7245
  this.containerIndex = projection.containerIndex;
7227
7246
  this.slotIndex = projection.slotIndex;
@@ -7258,6 +7277,44 @@ var DragIntentEngine = class {
7258
7277
  getContainerChildren(containerId) {
7259
7278
  return this.containerIndex.get(containerId)?.children ?? [];
7260
7279
  }
7280
+ /**
7281
+ * Every container id this engine would accept as a drop target for the
7282
+ * active node, at the current pointer position or any other — i.e. the
7283
+ * full set `resolvePointerContainer` draws from, not a snapshot of one
7284
+ * resolve() call. Intended for the visual layer to draw a guide that
7285
+ * matches exactly what the engine can land on, without re-deriving the
7286
+ * eligibility rule itself.
7287
+ *
7288
+ * Always queries with `laneLock: true`. That is not an approximation of
7289
+ * the "real" default — it IS the real answer for every reachable caller:
7290
+ *
7291
+ * - With a row lock active (`laneAllowedContainers` set), `isLaneContainer`
7292
+ * restricts eligibility to that set, so this returns exactly
7293
+ * `{srcContainerId}` — the ARCH-1071 lock, reproduced faithfully.
7294
+ * - Without a row lock, `laneAllowedContainers` is null and
7295
+ * `isLaneContainer` returns true unconditionally, so `laneLock`'s value
7296
+ * makes no difference — passing `true` costs nothing here.
7297
+ * - The engine's own 'nest' plane does call `isEligibleDropContainer` with
7298
+ * `laneLock: false` (see `peekNestIntent` → `resolvePointerContainer`),
7299
+ * but that plane is unreachable for a locked drag: `computeAllowNest`
7300
+ * (useDragVisualLoop.ts) short-circuits `rowLocked` drags before
7301
+ * `setTargetPlane('nest')` is ever called, so `targetPlane` stays
7302
+ * 'lane' for the drag's whole lifetime. If that guard is ever removed,
7303
+ * this method will under-report nest-only targets for locked drags —
7304
+ * keep the two in sync.
7305
+ *
7306
+ * Cached after the first call: every input (containerIndex, slotIndex,
7307
+ * activeId, laneAllowedContainers, blockedContainerIds) is fixed at
7308
+ * construction and never changes for the life of a drag.
7309
+ */
7310
+ getValidDropContainerIds() {
7311
+ if (this.validDropContainerIds) return this.validDropContainerIds;
7312
+ const ids2 = /* @__PURE__ */ new Set();
7313
+ for (const id of this.containerIndex.keys()) {
7314
+ if (this.isEligibleDropContainer(id, true)) ids2.add(id);
7315
+ }
7316
+ return this.validDropContainerIds = ids2;
7317
+ }
7261
7318
  setTargetPlane(plane) {
7262
7319
  this.targetPlane = plane;
7263
7320
  }
@@ -7276,6 +7333,17 @@ var DragIntentEngine = class {
7276
7333
  isInvalidDropContainer(containerId) {
7277
7334
  return containerId === this.activeId || this.isContainerDescendantOf(containerId, this.activeId);
7278
7335
  }
7336
+ /**
7337
+ * The full "can this container accept the active node this drag" predicate.
7338
+ * SINGLE definition — every caller routes through it so no two parts of the
7339
+ * system can disagree about which containers are droppable.
7340
+ */
7341
+ isEligibleDropContainer(containerId, laneLock) {
7342
+ if (this.isInvalidDropContainer(containerId)) return false;
7343
+ if (this.isBlockedContainer(containerId)) return false;
7344
+ if (laneLock && !this.isLaneContainer(containerId)) return false;
7345
+ return this.hasUsableSlots(containerId);
7346
+ }
7279
7347
  /**
7280
7348
  * Resolve the drag intent from the current pointer position.
7281
7349
  *
@@ -7363,9 +7431,7 @@ var DragIntentEngine = class {
7363
7431
  const childNode = this.getNodeRect(childId);
7364
7432
  if (!childNode) continue;
7365
7433
  if (!pointInRect(px, py, childNode)) continue;
7366
- const relX = (px - childNode.left) / childNode.width;
7367
- const relY = (py - childNode.top) / childNode.height;
7368
- if (relX > NEST_EDGE_FRACTION && relX < 1 - NEST_EDGE_FRACTION && relY > NEST_EDGE_FRACTION && relY < 1 - NEST_EDGE_FRACTION) {
7434
+ if (isInNestZone(px, py, childNode)) {
7369
7435
  return {
7370
7436
  containerId: pointerContainerId,
7371
7437
  index: container.children.indexOf(childId),
@@ -7379,11 +7445,8 @@ var DragIntentEngine = class {
7379
7445
  resolvePointerContainer(px, py, laneLock) {
7380
7446
  const pointerContainers = [];
7381
7447
  for (const [id, node] of this.containerIndex) {
7382
- if (this.isInvalidDropContainer(id)) continue;
7383
- if (this.isBlockedContainer(id)) continue;
7384
- if (laneLock && !this.isLaneContainer(id)) continue;
7385
7448
  if (!pointInRect(px, py, node.rect)) continue;
7386
- if (this.hasUsableSlots(id) === false) continue;
7449
+ if (!this.isEligibleDropContainer(id, laneLock)) continue;
7387
7450
  const r = node.rect;
7388
7451
  const edgeDistX = Math.min(px - r.left, r.right - px);
7389
7452
  const edgeDistY = Math.min(py - r.top, r.bottom - py);
@@ -7400,11 +7463,11 @@ var DragIntentEngine = class {
7400
7463
  let pointerContainerId = pointerContainers[0]?.id ?? null;
7401
7464
  if (!pointerContainerId && this.lastIntent?.mode === "reorder") {
7402
7465
  const lastContainer = this.containerIndex.get(this.lastIntent.containerId);
7403
- if (lastContainer && !this.isInvalidDropContainer(lastContainer.id) && !this.isBlockedContainer(lastContainer.id) && (!laneLock || this.isLaneContainer(lastContainer.id)) && this.hasUsableSlots(lastContainer.id) && pointInExpandedRect(px, py, lastContainer.rect, LAST_CONTAINER_REENTRY_SLOP_PX)) {
7466
+ if (lastContainer && this.isEligibleDropContainer(lastContainer.id, laneLock) && pointInExpandedRect(px, py, lastContainer.rect, LAST_CONTAINER_REENTRY_SLOP_PX)) {
7404
7467
  pointerContainerId = lastContainer.id;
7405
7468
  }
7406
7469
  }
7407
- if (this.originContainerId && this.originContainerId !== pointerContainerId && !this.isInvalidDropContainer(this.originContainerId) && !this.isBlockedContainer(this.originContainerId) && (!laneLock || this.isLaneContainer(this.originContainerId)) && this.hasUsableSlots(this.originContainerId)) {
7470
+ if (this.originContainerId && this.originContainerId !== pointerContainerId && this.isEligibleDropContainer(this.originContainerId, laneLock)) {
7408
7471
  const originNode = this.containerIndex.get(this.originContainerId);
7409
7472
  const currentNode = pointerContainerId ? this.containerIndex.get(pointerContainerId) : null;
7410
7473
  const originIsDeeper = originNode && (!currentNode || originNode.depth >= currentNode.depth);
@@ -7817,7 +7880,8 @@ function useDragVisualLoop({
7817
7880
  refs,
7818
7881
  getCommitLaneSnapshot,
7819
7882
  setInsideRect,
7820
- setDropLine
7883
+ setDropLine,
7884
+ setActiveLaneContainerId
7821
7885
  }) {
7822
7886
  useEffect8(() => {
7823
7887
  const engine = refs.engineRef.current;
@@ -7825,6 +7889,11 @@ function useDragVisualLoop({
7825
7889
  if (!activeDrag || !engine || !snapshot) return;
7826
7890
  let rafId;
7827
7891
  const animator = refs.animatorRef.current;
7892
+ const publishActiveLane = (containerId) => {
7893
+ if (refs.activeLaneContainerIdRef.current === containerId) return;
7894
+ refs.activeLaneContainerIdRef.current = containerId;
7895
+ setActiveLaneContainerId(containerId);
7896
+ };
7828
7897
  const setTargetPlane = (nextPlane) => {
7829
7898
  const prevPlane = refs.targetPlaneRef.current;
7830
7899
  if (prevPlane === nextPlane) return;
@@ -7843,6 +7912,7 @@ function useDragVisualLoop({
7843
7912
  const safeZone = refs.safeZoneRef.current;
7844
7913
  if (isPointerBlocked(px, py, safeZone)) {
7845
7914
  setTargetPlane("lane");
7915
+ publishActiveLane(null);
7846
7916
  refs.lastIntentRef.current = null;
7847
7917
  refs.visualIntentRef.current = null;
7848
7918
  refs.nestCandidateRef.current = null;
@@ -7884,6 +7954,7 @@ function useDragVisualLoop({
7884
7954
  let intent = engine.resolve(px, py, window.scrollX, window.scrollY, snapshot.scroll.x, snapshot.scroll.y);
7885
7955
  if (!intent) {
7886
7956
  setTargetPlane("lane");
7957
+ publishActiveLane(null);
7887
7958
  refs.lastIntentRef.current = null;
7888
7959
  refs.visualIntentRef.current = null;
7889
7960
  refs.nestCandidateRef.current = null;
@@ -7909,6 +7980,7 @@ function useDragVisualLoop({
7909
7980
  const targetId = intent.slotId.split("::nest::")[1] ?? "";
7910
7981
  if (!targetId) {
7911
7982
  setTargetPlane("lane");
7983
+ publishActiveLane(null);
7912
7984
  rafId = requestAnimationFrame(tick);
7913
7985
  return;
7914
7986
  }
@@ -7930,12 +8002,14 @@ function useDragVisualLoop({
7930
8002
  };
7931
8003
  refs.visualIntentRef.current = intent;
7932
8004
  refs.lastIntentRef.current = intent;
8005
+ publishActiveLane(intent.containerId);
7933
8006
  } else {
7934
8007
  setTargetPlane("lane");
7935
8008
  refs.nestCandidateRef.current = null;
7936
8009
  refs.nestTargetRef.current = null;
7937
8010
  refs.lastIntentRef.current = intent;
7938
8011
  refs.visualIntentRef.current = intent;
8012
+ publishActiveLane(intent.containerId);
7939
8013
  setInsideRect(null);
7940
8014
  const proj = refs.projectionRef.current;
7941
8015
  const slot = proj.slotIndex.get(intent.slotId);
@@ -8242,7 +8316,8 @@ function useDragLifecycle({
8242
8316
  syncHistoryAvailability,
8243
8317
  resolveNodeElement,
8244
8318
  setInsideRect,
8245
- setDropLine
8319
+ setDropLine,
8320
+ setActiveLaneContainerId
8246
8321
  }) {
8247
8322
  const [activeDrag, setActiveDrag] = useState5(null);
8248
8323
  const clearDragState = useCallback6(() => {
@@ -8253,13 +8328,12 @@ function useDragLifecycle({
8253
8328
  refs.snapshotRef.current = null;
8254
8329
  refs.projectionRef.current = EMPTY_PROJECTION;
8255
8330
  refs.engineRef.current = null;
8256
- refs.blockedContainerIdsRef.current = /* @__PURE__ */ new Set();
8257
8331
  refs.lastIntentRef.current = null;
8258
8332
  refs.visualIntentRef.current = null;
8259
8333
  refs.sourceContainerIdRef.current = null;
8334
+ refs.activeLaneContainerIdRef.current = null;
8260
8335
  refs.rowLockedRef.current = false;
8261
8336
  refs.dragSessionAnchorRef.current = null;
8262
- refs.lockRef.current = { containerId: null, isCrossUnlocked: false, exitSince: null, unlockUntilTs: null };
8263
8337
  refs.targetPlaneRef.current = "lane";
8264
8338
  refs.nestCandidateRef.current = null;
8265
8339
  refs.nestTargetRef.current = null;
@@ -8270,6 +8344,7 @@ function useDragLifecycle({
8270
8344
  refs.safeZoneRef.current = null;
8271
8345
  setInsideRect(null);
8272
8346
  setDropLine(null);
8347
+ setActiveLaneContainerId(null);
8273
8348
  setActiveDrag(null);
8274
8349
  resume();
8275
8350
  }, [clearCommitLaneCache, resume]);
@@ -8327,7 +8402,6 @@ function useDragLifecycle({
8327
8402
  const sourceCommitLane = getCommitLaneSnapshot(srcContainerId);
8328
8403
  const startPosition = getCurrentCommitPlacement(anchorNodeId, srcContainerId);
8329
8404
  const blockedContainerIds = buildBlockedStructuralContainerIds(proj, refs.nodeMapRef.current, srcContainerId);
8330
- refs.blockedContainerIdsRef.current = blockedContainerIds;
8331
8405
  const engineOptions = buildDragIntentEngineOptions(anchorScanned.element, srcContainerId, blockedContainerIds);
8332
8406
  refs.rowLockedRef.current = !!engineOptions.laneAllowedContainers;
8333
8407
  refs.engineRef.current = createDragIntentEngine(proj, anchorNodeId, engineOptions);
@@ -8336,7 +8410,6 @@ function useDragLifecycle({
8336
8410
  refs.sourceContainerIdRef.current = srcContainerId;
8337
8411
  refs.sourceCommitLaneRef.current = sourceCommitLane;
8338
8412
  refs.dragSessionAnchorRef.current = anchor;
8339
- refs.lockRef.current = { containerId: srcContainerId, isCrossUnlocked: true, exitSince: null, unlockUntilTs: null };
8340
8413
  refs.dragStartPositionRef.current = startPosition;
8341
8414
  refs.safeZoneRef.current = resolveSafeZoneRect();
8342
8415
  pause();
@@ -8895,6 +8968,7 @@ function commitExternalDrop(x, y, engine, component, refs, resolveNodeElement, s
8895
8968
  // src/client/dnd/design-mode/DesignModeOverlay.tsx
8896
8969
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
8897
8970
  var DROP_ANIMATION = { duration: DURATION_MS, easing: EASING };
8971
+ var EMPTY_CONTAINER_ID_SET = /* @__PURE__ */ new Set();
8898
8972
  function resizeCursorFromHandle(handle) {
8899
8973
  switch (handle) {
8900
8974
  case "n":
@@ -9182,13 +9256,9 @@ function DesignModeOverlay() {
9182
9256
  const sensors = useSensors(
9183
9257
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
9184
9258
  );
9185
- const collisionDetection = useCallback8(
9186
- (args) => createDesignModeCollisionDetection(refs.blockedContainerIdsRef.current)(args),
9187
- []
9188
- // eslint-disable-line react-hooks/exhaustive-deps -- refs are stable
9189
- );
9190
9259
  const [insideRect, setInsideRect] = useState6(null);
9191
9260
  const [dropLine, setDropLine] = useState6(null);
9261
+ const [activeLaneContainerId, setActiveLaneContainerId] = useState6(null);
9192
9262
  useEffect11(() => {
9193
9263
  if (!enabled) return;
9194
9264
  const fn = (e) => {
@@ -9224,8 +9294,43 @@ function DesignModeOverlay() {
9224
9294
  syncHistoryAvailability,
9225
9295
  resolveNodeElement,
9226
9296
  setInsideRect,
9227
- setDropLine
9297
+ setDropLine,
9298
+ setActiveLaneContainerId
9228
9299
  });
9300
+ const dropCandidateContainerIds = useMemo6(
9301
+ () => activeDrag ? refs.engineRef.current?.getValidDropContainerIds() ?? null : null,
9302
+ [activeDrag]
9303
+ // eslint-disable-line react-hooks/exhaustive-deps -- refs are stable
9304
+ );
9305
+ const visibleCandidateContainerIds = useMemo6(() => {
9306
+ if (!dropCandidateContainerIds) return null;
9307
+ const rects = refs.snapshotRef.current?.rects;
9308
+ if (!rects) return dropCandidateContainerIds;
9309
+ const vw = window.innerWidth;
9310
+ const vh = window.innerHeight;
9311
+ const visible = /* @__PURE__ */ new Set();
9312
+ for (const id of dropCandidateContainerIds) {
9313
+ const r = rects.get(id);
9314
+ if (!r) continue;
9315
+ if (r.right < 0 || r.bottom < 0 || r.left > vw || r.top > vh) continue;
9316
+ visible.add(id);
9317
+ }
9318
+ return visible;
9319
+ }, [dropCandidateContainerIds]);
9320
+ const dragGuide = useMemo6(() => {
9321
+ if (!activeDrag) return null;
9322
+ const snapshot = refs.snapshotRef.current;
9323
+ if (!snapshot) return null;
9324
+ const activeChildIds = activeLaneContainerId ? refs.engineRef.current?.getContainerChildren(activeLaneContainerId) ?? [] : [];
9325
+ return {
9326
+ rects: snapshot.rects,
9327
+ startScroll: snapshot.scroll,
9328
+ candidateContainerIds: visibleCandidateContainerIds ?? EMPTY_CONTAINER_ID_SET,
9329
+ activeContainerId: activeLaneContainerId,
9330
+ activeChildIds,
9331
+ draggingNodeId: activeDrag.nodeId
9332
+ };
9333
+ }, [activeDrag, activeLaneContainerId, visibleCandidateContainerIds]);
9229
9334
  useEffect11(() => {
9230
9335
  if (!enabled || activeDrag) return;
9231
9336
  if (undoRequestId === refs.processedUndoIdRef.current) return;
@@ -9288,7 +9393,7 @@ function DesignModeOverlay() {
9288
9393
  safeZoneRef: refs.safeZoneRef,
9289
9394
  autoScrollStateRef: refs.autoScrollStateRef
9290
9395
  });
9291
- useDragVisualLoop({ activeDrag, refs, getCommitLaneSnapshot, setInsideRect, setDropLine });
9396
+ useDragVisualLoop({ activeDrag, refs, getCommitLaneSnapshot, setInsideRect, setDropLine, setActiveLaneContainerId });
9292
9397
  useExternalDragLoop({
9293
9398
  externalDrag,
9294
9399
  refs,
@@ -9308,7 +9413,6 @@ function DesignModeOverlay() {
9308
9413
  DndContext,
9309
9414
  {
9310
9415
  sensors,
9311
- collisionDetection,
9312
9416
  autoScroll: false,
9313
9417
  onDragStart: handleDragStart,
9314
9418
  onDragEnd: handleDragEnd,
@@ -9317,10 +9421,8 @@ function DesignModeOverlay() {
9317
9421
  /* @__PURE__ */ jsx4(
9318
9422
  ElementHighlight,
9319
9423
  {
9320
- elements,
9321
- draggingNodeId: activeDrag?.nodeId ?? null,
9322
- activeEl: refs.activeElRef.current,
9323
- arrangeSelectedNodeId,
9424
+ selected: arrangeSelectedNodeId ? nodeMap.get(arrangeSelectedNodeId) ?? null : null,
9425
+ dragGuide,
9324
9426
  externalDragActive: externalDrag !== null,
9325
9427
  onResizeHandlePointerDown: handleResizeHandlePointerDown
9326
9428
  }