@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.
@@ -616,44 +616,6 @@ function getFirstCollision(collisions, property) {
616
616
  const [firstCollision] = collisions;
617
617
  return property ? firstCollision[property] : firstCollision;
618
618
  }
619
- function centerOfRectangle(rect, left, top) {
620
- if (left === void 0) {
621
- left = rect.left;
622
- }
623
- if (top === void 0) {
624
- top = rect.top;
625
- }
626
- return {
627
- x: left + rect.width * 0.5,
628
- y: top + rect.height * 0.5
629
- };
630
- }
631
- var closestCenter = (_ref) => {
632
- let {
633
- collisionRect,
634
- droppableRects,
635
- droppableContainers
636
- } = _ref;
637
- const centerRect = centerOfRectangle(collisionRect, collisionRect.left, collisionRect.top);
638
- const collisions = [];
639
- for (const droppableContainer of droppableContainers) {
640
- const {
641
- id
642
- } = droppableContainer;
643
- const rect = droppableRects.get(id);
644
- if (rect) {
645
- const distBetween = distanceBetween(centerOfRectangle(rect), centerRect);
646
- collisions.push({
647
- id,
648
- data: {
649
- droppableContainer,
650
- value: distBetween
651
- }
652
- });
653
- }
654
- }
655
- return collisions.sort(sortCollisionsAsc);
656
- };
657
619
  var closestCorners = (_ref) => {
658
620
  let {
659
621
  collisionRect,
@@ -5582,6 +5544,7 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
5582
5544
  var HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
5583
5545
  var HANDLE_HIT_SIZE = 24;
5584
5546
  var HANDLE_KNOB_SIZE = 8;
5547
+ var EMPTY_SCROLL_DELTA = { x: NaN, y: NaN };
5585
5548
  var OVERLAY_ROOT_STYLE = {
5586
5549
  position: "fixed",
5587
5550
  inset: 0,
@@ -5655,17 +5618,39 @@ function handlePosition(dir) {
5655
5618
  };
5656
5619
  return positions[dir];
5657
5620
  }
5658
- function getOutlineStyle(rect, isSelected, inspectorTheme) {
5621
+ function getSelectedOutlineStyle(rect, inspectorTheme) {
5659
5622
  return {
5660
5623
  ...OUTLINE_BASE_STYLE,
5661
5624
  left: rect.left,
5662
5625
  top: rect.top,
5663
5626
  width: rect.width,
5664
5627
  height: rect.height,
5665
- border: isSelected ? `2px solid ${inspectorTheme.accent}` : `1px dashed ${inspectorTheme.shared}`,
5666
- backgroundColor: isSelected ? inspectorTheme.accentSoft : "transparent"
5628
+ border: `2px solid ${inspectorTheme.accent}`,
5629
+ backgroundColor: inspectorTheme.accentSoft
5667
5630
  };
5668
5631
  }
5632
+ function getGuideOutlineStyle(rect, tier, inspectorTheme) {
5633
+ const base = {
5634
+ ...OUTLINE_BASE_STYLE,
5635
+ left: rect.left,
5636
+ top: rect.top,
5637
+ width: rect.width,
5638
+ height: rect.height
5639
+ };
5640
+ switch (tier) {
5641
+ case "candidate":
5642
+ return { ...base, border: `1px solid ${inspectorTheme.accent}`, opacity: 0.2 };
5643
+ case "lane-child":
5644
+ return { ...base, border: `1px dashed ${inspectorTheme.accent}`, opacity: 0.45 };
5645
+ case "lane":
5646
+ return {
5647
+ ...base,
5648
+ border: `1.5px solid ${inspectorTheme.accent}`,
5649
+ backgroundColor: inspectorTheme.accentSoft,
5650
+ opacity: 0.75
5651
+ };
5652
+ }
5653
+ }
5669
5654
  function getResizeHandleStyle(position) {
5670
5655
  return {
5671
5656
  ...RESIZE_HANDLE_BASE_STYLE,
@@ -5688,40 +5673,58 @@ function getLabelStyle(labelLeft, labelTop, labelColor) {
5688
5673
  backgroundColor: labelColor
5689
5674
  };
5690
5675
  }
5676
+ function projectDragRect(dragGuide, nodeId, sdx, sdy) {
5677
+ const r = dragGuide.rects.get(nodeId);
5678
+ if (!r) return null;
5679
+ return { left: r.left - sdx, top: r.top - sdy, width: r.width, height: r.height };
5680
+ }
5691
5681
  function ElementHighlight({
5692
- elements,
5693
- draggingNodeId,
5694
- activeEl,
5695
- arrangeSelectedNodeId,
5682
+ selected,
5683
+ dragGuide,
5696
5684
  externalDragActive,
5697
5685
  onResizeHandlePointerDown
5698
5686
  }) {
5699
5687
  const inspectorTheme = useDesignModeStore((s) => s.inspectorTheme);
5700
- const internalDragInProgress = draggingNodeId !== null;
5701
- const overlayVisible = arrangeSelectedNodeId !== null || internalDragInProgress;
5702
5688
  const containerRef = (0, import_react7.useRef)(null);
5703
5689
  const rafRef = (0, import_react7.useRef)(0);
5690
+ const lastScrollDeltaRef = (0, import_react7.useRef)(EMPTY_SCROLL_DELTA);
5704
5691
  const syncPositions = (0, import_react7.useCallback)(() => {
5705
5692
  const container = containerRef.current;
5706
5693
  if (!container) return;
5707
- const outlines = container.querySelectorAll("[data-outline-for]");
5708
- for (const outline of outlines) {
5709
- const nodeId = outline.getAttribute("data-outline-for");
5710
- const el = elements.find((e) => e.nodeId === nodeId);
5711
- if (!el) continue;
5712
- const rect = el.element.getBoundingClientRect();
5713
- outline.style.left = `${rect.left}px`;
5714
- outline.style.top = `${rect.top}px`;
5715
- outline.style.width = `${rect.width}px`;
5716
- outline.style.height = `${rect.height}px`;
5717
- const label = outline.nextElementSibling;
5718
- if (label) {
5719
- label.style.left = `${rect.left}px`;
5720
- label.style.top = `${rect.top}px`;
5694
+ if (dragGuide) {
5695
+ const sdx = window.scrollX - dragGuide.startScroll.x;
5696
+ const sdy = window.scrollY - dragGuide.startScroll.y;
5697
+ if (sdx === lastScrollDeltaRef.current.x && sdy === lastScrollDeltaRef.current.y) return;
5698
+ lastScrollDeltaRef.current = { x: sdx, y: sdy };
5699
+ const outlines = container.querySelectorAll("[data-guide-for]");
5700
+ for (const outline2 of outlines) {
5701
+ const nodeId = outline2.getAttribute("data-guide-for");
5702
+ const r = dragGuide.rects.get(nodeId);
5703
+ if (!r) continue;
5704
+ outline2.style.left = `${r.left - sdx}px`;
5705
+ outline2.style.top = `${r.top - sdy}px`;
5721
5706
  }
5707
+ return;
5722
5708
  }
5723
- }, [elements]);
5709
+ if (!selected) return;
5710
+ const rect2 = selected.element.getBoundingClientRect();
5711
+ const outline = container.querySelector("[data-outline-for]");
5712
+ if (outline) {
5713
+ outline.style.left = `${rect2.left}px`;
5714
+ outline.style.top = `${rect2.top}px`;
5715
+ outline.style.width = `${rect2.width}px`;
5716
+ outline.style.height = `${rect2.height}px`;
5717
+ }
5718
+ const label = container.querySelector("[data-drag-handle]");
5719
+ if (label) {
5720
+ label.style.left = `${rect2.left + 12}px`;
5721
+ label.style.top = `${Math.max(0, rect2.top - 20)}px`;
5722
+ }
5723
+ }, [dragGuide, selected]);
5724
+ const loopActive = !externalDragActive && (dragGuide !== null || selected !== null);
5724
5725
  (0, import_react7.useEffect)(() => {
5726
+ if (!loopActive) return;
5727
+ lastScrollDeltaRef.current = EMPTY_SCROLL_DELTA;
5725
5728
  let running = true;
5726
5729
  const loop = () => {
5727
5730
  if (!running) return;
@@ -5733,81 +5736,113 @@ function ElementHighlight({
5733
5736
  running = false;
5734
5737
  cancelAnimationFrame(rafRef.current);
5735
5738
  };
5736
- }, [syncPositions]);
5737
- if (!overlayVisible) return null;
5738
- if (externalDragActive) return null;
5739
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5740
- "div",
5741
- {
5742
- ref: containerRef,
5743
- "data-design-mode-ui": true,
5744
- style: OVERLAY_ROOT_STYLE,
5745
- children: elements.map((el) => {
5746
- if (el.nodeId === draggingNodeId) return null;
5747
- if (activeEl && activeEl !== el.element && activeEl.contains(el.element)) return null;
5748
- const isSelected = el.nodeId === arrangeSelectedNodeId;
5749
- if (!isSelected && !internalDragInProgress) return null;
5750
- const rect = el.element.getBoundingClientRect();
5751
- const showLabel = isSelected;
5752
- const labelTop = isSelected ? Math.max(0, rect.top - 20) : rect.top;
5753
- const labelLeft = isSelected ? rect.left + 12 : rect.left;
5754
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
5755
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5756
- "div",
5757
- {
5758
- "data-outline-for": el.nodeId,
5759
- "data-design-mode-ui": true,
5760
- style: getOutlineStyle(rect, isSelected, inspectorTheme),
5761
- children: isSelected && el.draggable && HANDLES.map((dir) => {
5762
- const pos = handlePosition(dir);
5763
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5764
- "div",
5765
- {
5766
- "data-design-mode-ui": true,
5767
- "data-resize-handle": dir,
5768
- onPointerDown: (e) => {
5769
- e.stopPropagation();
5770
- onResizeHandlePointerDown?.(e, el.nodeId, dir);
5771
- },
5772
- style: getResizeHandleStyle(pos),
5773
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5774
- "span",
5775
- {
5776
- "data-design-mode-ui": true,
5777
- style: getResizeKnobStyle(inspectorTheme.accent)
5778
- }
5779
- )
5780
- },
5781
- dir
5782
- );
5783
- })
5784
- }
5785
- ),
5786
- showLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
5739
+ }, [loopActive, syncPositions]);
5740
+ if (!loopActive) return null;
5741
+ if (dragGuide) {
5742
+ const sdx = window.scrollX - dragGuide.startScroll.x;
5743
+ const sdy = window.scrollY - dragGuide.startScroll.y;
5744
+ const { candidateContainerIds, activeContainerId, activeChildIds, draggingNodeId } = dragGuide;
5745
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: containerRef, "data-design-mode-ui": true, style: OVERLAY_ROOT_STYLE, children: [
5746
+ [...candidateContainerIds].map((id) => {
5747
+ if (id === activeContainerId || id === draggingNodeId) return null;
5748
+ const rect2 = projectDragRect(dragGuide, id, sdx, sdy);
5749
+ if (!rect2) return null;
5750
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5751
+ "div",
5752
+ {
5753
+ "data-guide-for": id,
5754
+ "data-guide-tier": "candidate",
5755
+ "data-design-mode-ui": true,
5756
+ style: getGuideOutlineStyle(rect2, "candidate", inspectorTheme)
5757
+ },
5758
+ `candidate:${id}`
5759
+ );
5760
+ }),
5761
+ activeContainerId !== null && activeChildIds.map((id) => {
5762
+ if (id === draggingNodeId) return null;
5763
+ const rect2 = projectDragRect(dragGuide, id, sdx, sdy);
5764
+ if (!rect2) return null;
5765
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5766
+ "div",
5767
+ {
5768
+ "data-guide-for": id,
5769
+ "data-guide-tier": "lane-child",
5770
+ "data-design-mode-ui": true,
5771
+ style: getGuideOutlineStyle(rect2, "lane-child", inspectorTheme)
5772
+ },
5773
+ `lane-child:${id}`
5774
+ );
5775
+ }),
5776
+ activeContainerId !== null && activeContainerId !== draggingNodeId && (() => {
5777
+ const rect2 = projectDragRect(dragGuide, activeContainerId, sdx, sdy);
5778
+ if (!rect2) return null;
5779
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5780
+ "div",
5781
+ {
5782
+ "data-guide-for": activeContainerId,
5783
+ "data-guide-tier": "lane",
5784
+ "data-design-mode-ui": true,
5785
+ style: getGuideOutlineStyle(rect2, "lane", inspectorTheme)
5786
+ },
5787
+ `lane:${activeContainerId}`
5788
+ );
5789
+ })()
5790
+ ] });
5791
+ }
5792
+ if (!selected) return null;
5793
+ const rect = selected.element.getBoundingClientRect();
5794
+ const labelTop = Math.max(0, rect.top - 20);
5795
+ const labelLeft = rect.left + 12;
5796
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: containerRef, "data-design-mode-ui": true, style: OVERLAY_ROOT_STYLE, children: [
5797
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5798
+ "div",
5799
+ {
5800
+ "data-outline-for": selected.nodeId,
5801
+ "data-design-mode-ui": true,
5802
+ style: getSelectedOutlineStyle(rect, inspectorTheme),
5803
+ children: selected.draggable && HANDLES.map((dir) => {
5804
+ const pos = handlePosition(dir);
5805
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5787
5806
  "div",
5788
5807
  {
5789
5808
  "data-design-mode-ui": true,
5790
- "data-drag-handle": el.nodeId,
5791
- style: getLabelStyle(
5792
- labelLeft,
5793
- labelTop,
5794
- isSelected ? inspectorTheme.accent : inspectorTheme.hover
5795
- ),
5796
- children: [
5797
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: LABEL_ICON_STYLE, children: "\u283F" }),
5798
- el.label,
5799
- isSelected && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: LABEL_DIMENSIONS_STYLE, children: [
5800
- Math.round(rect.width),
5801
- "\xD7",
5802
- Math.round(rect.height)
5803
- ] })
5804
- ]
5805
- }
5806
- )
5807
- ] }, el.nodeId);
5808
- })
5809
- }
5810
- );
5809
+ "data-resize-handle": dir,
5810
+ onPointerDown: (e) => {
5811
+ e.stopPropagation();
5812
+ onResizeHandlePointerDown?.(e, selected.nodeId, dir);
5813
+ },
5814
+ style: getResizeHandleStyle(pos),
5815
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5816
+ "span",
5817
+ {
5818
+ "data-design-mode-ui": true,
5819
+ style: getResizeKnobStyle(inspectorTheme.accent)
5820
+ }
5821
+ )
5822
+ },
5823
+ dir
5824
+ );
5825
+ })
5826
+ }
5827
+ ),
5828
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
5829
+ "div",
5830
+ {
5831
+ "data-design-mode-ui": true,
5832
+ "data-drag-handle": selected.nodeId,
5833
+ style: getLabelStyle(labelLeft, labelTop, inspectorTheme.accent),
5834
+ children: [
5835
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: LABEL_ICON_STYLE, children: "\u283F" }),
5836
+ selected.label,
5837
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: LABEL_DIMENSIONS_STYLE, children: [
5838
+ Math.round(rect.width),
5839
+ "\xD7",
5840
+ Math.round(rect.height)
5841
+ ] })
5842
+ ]
5843
+ }
5844
+ )
5845
+ ] });
5811
5846
  }
5812
5847
 
5813
5848
  // src/client/dnd/design-mode/projection.ts
@@ -6601,30 +6636,6 @@ function applyDndCommandPlacement(command, direction, resolveNodeElement) {
6601
6636
  return true;
6602
6637
  }
6603
6638
 
6604
- // src/client/dnd/design-mode/design-mode-collision.ts
6605
- function filterDroppables(droppableContainers, blockedContainerIds) {
6606
- if (!blockedContainerIds || blockedContainerIds.size === 0) return droppableContainers;
6607
- return droppableContainers.filter((container) => !blockedContainerIds.has(String(container.id)));
6608
- }
6609
- function getCollisions(detector, args) {
6610
- return detector(args);
6611
- }
6612
- function createDesignModeCollisionDetection(blockedContainerIds) {
6613
- return (args) => {
6614
- const droppableContainers = filterDroppables(args.droppableContainers, blockedContainerIds);
6615
- if (droppableContainers.length === 0) return [];
6616
- const scopedArgs = {
6617
- ...args,
6618
- droppableContainers
6619
- };
6620
- const pointerHits = getCollisions(pointerWithin, scopedArgs);
6621
- if (pointerHits.length > 0) return pointerHits;
6622
- const cornerHits = getCollisions(closestCorners, scopedArgs);
6623
- if (cornerHits.length > 0) return cornerHits;
6624
- return getCollisions(closestCenter, scopedArgs);
6625
- };
6626
- }
6627
-
6628
6639
  // src/client/dnd/design-mode/arrange-session.ts
6629
6640
  function serializeArrangeChange(event, index) {
6630
6641
  return {
@@ -6903,9 +6914,9 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6903
6914
  const lastIntentRef = (0, import_react11.useRef)(null);
6904
6915
  const visualIntentRef = (0, import_react11.useRef)(null);
6905
6916
  const sourceContainerIdRef = (0, import_react11.useRef)(null);
6917
+ const activeLaneContainerIdRef = (0, import_react11.useRef)(null);
6906
6918
  const rowLockedRef = (0, import_react11.useRef)(false);
6907
6919
  const dragSessionAnchorRef = (0, import_react11.useRef)(null);
6908
- const lockRef = (0, import_react11.useRef)({ containerId: null, isCrossUnlocked: false, exitSince: null, unlockUntilTs: null });
6909
6920
  const targetPlaneRef = (0, import_react11.useRef)("lane");
6910
6921
  const nestCandidateRef = (0, import_react11.useRef)(null);
6911
6922
  const nestTargetRef = (0, import_react11.useRef)(null);
@@ -6915,7 +6926,6 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6915
6926
  const sourceCommitLaneRef = (0, import_react11.useRef)(null);
6916
6927
  const commitLaneCacheRef = (0, import_react11.useRef)(/* @__PURE__ */ new Map());
6917
6928
  const safeZoneRef = (0, import_react11.useRef)(null);
6918
- const blockedContainerIdsRef = (0, import_react11.useRef)(/* @__PURE__ */ new Set());
6919
6929
  const historyRef = (0, import_react11.useRef)(new DndCommandHistory());
6920
6930
  const processedUndoIdRef = (0, import_react11.useRef)(undoRequestId);
6921
6931
  const processedRedoIdRef = (0, import_react11.useRef)(redoRequestId);
@@ -6935,9 +6945,9 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6935
6945
  lastIntentRef,
6936
6946
  visualIntentRef,
6937
6947
  sourceContainerIdRef,
6948
+ activeLaneContainerIdRef,
6938
6949
  rowLockedRef,
6939
6950
  dragSessionAnchorRef,
6940
- lockRef,
6941
6951
  targetPlaneRef,
6942
6952
  nestCandidateRef,
6943
6953
  nestTargetRef,
@@ -6947,7 +6957,6 @@ function useOverlayRefs(undoRequestId, redoRequestId) {
6947
6957
  sourceCommitLaneRef,
6948
6958
  commitLaneCacheRef,
6949
6959
  safeZoneRef,
6950
- blockedContainerIdsRef,
6951
6960
  historyRef,
6952
6961
  processedUndoIdRef,
6953
6962
  processedRedoIdRef,
@@ -7215,6 +7224,11 @@ function pointInRect(px, py, r) {
7215
7224
  function pointInExpandedRect(px, py, r, inset) {
7216
7225
  return px >= r.left - inset && px <= r.right + inset && py >= r.top - inset && py <= r.bottom + inset;
7217
7226
  }
7227
+ function isInNestZone(px, py, rect) {
7228
+ const relX = (px - rect.left) / rect.width;
7229
+ const relY = (py - rect.top) / rect.height;
7230
+ return relX > NEST_EDGE_FRACTION && relX < 1 - NEST_EDGE_FRACTION && relY > NEST_EDGE_FRACTION && relY < 1 - NEST_EDGE_FRACTION;
7231
+ }
7218
7232
  function buildSlotQuadtree(slotIndex) {
7219
7233
  const rects = /* @__PURE__ */ new Map();
7220
7234
  for (const [id, slot] of slotIndex) {
@@ -7257,6 +7271,11 @@ var DragIntentEngine = class {
7257
7271
  __publicField(this, "laneAllowedContainers");
7258
7272
  __publicField(this, "blockedContainerIds");
7259
7273
  __publicField(this, "targetPlane", "lane");
7274
+ // Memoized result of getValidDropContainerIds(). Safe to cache because every
7275
+ // input it reads (containerIndex, slotIndex, activeId, laneAllowedContainers,
7276
+ // blockedContainerIds) is fixed in the constructor and never mutated during
7277
+ // a drag.
7278
+ __publicField(this, "validDropContainerIds", null);
7260
7279
  this.projectionTree = projection.projectionTree;
7261
7280
  this.containerIndex = projection.containerIndex;
7262
7281
  this.slotIndex = projection.slotIndex;
@@ -7293,6 +7312,44 @@ var DragIntentEngine = class {
7293
7312
  getContainerChildren(containerId) {
7294
7313
  return this.containerIndex.get(containerId)?.children ?? [];
7295
7314
  }
7315
+ /**
7316
+ * Every container id this engine would accept as a drop target for the
7317
+ * active node, at the current pointer position or any other — i.e. the
7318
+ * full set `resolvePointerContainer` draws from, not a snapshot of one
7319
+ * resolve() call. Intended for the visual layer to draw a guide that
7320
+ * matches exactly what the engine can land on, without re-deriving the
7321
+ * eligibility rule itself.
7322
+ *
7323
+ * Always queries with `laneLock: true`. That is not an approximation of
7324
+ * the "real" default — it IS the real answer for every reachable caller:
7325
+ *
7326
+ * - With a row lock active (`laneAllowedContainers` set), `isLaneContainer`
7327
+ * restricts eligibility to that set, so this returns exactly
7328
+ * `{srcContainerId}` — the ARCH-1071 lock, reproduced faithfully.
7329
+ * - Without a row lock, `laneAllowedContainers` is null and
7330
+ * `isLaneContainer` returns true unconditionally, so `laneLock`'s value
7331
+ * makes no difference — passing `true` costs nothing here.
7332
+ * - The engine's own 'nest' plane does call `isEligibleDropContainer` with
7333
+ * `laneLock: false` (see `peekNestIntent` → `resolvePointerContainer`),
7334
+ * but that plane is unreachable for a locked drag: `computeAllowNest`
7335
+ * (useDragVisualLoop.ts) short-circuits `rowLocked` drags before
7336
+ * `setTargetPlane('nest')` is ever called, so `targetPlane` stays
7337
+ * 'lane' for the drag's whole lifetime. If that guard is ever removed,
7338
+ * this method will under-report nest-only targets for locked drags —
7339
+ * keep the two in sync.
7340
+ *
7341
+ * Cached after the first call: every input (containerIndex, slotIndex,
7342
+ * activeId, laneAllowedContainers, blockedContainerIds) is fixed at
7343
+ * construction and never changes for the life of a drag.
7344
+ */
7345
+ getValidDropContainerIds() {
7346
+ if (this.validDropContainerIds) return this.validDropContainerIds;
7347
+ const ids2 = /* @__PURE__ */ new Set();
7348
+ for (const id of this.containerIndex.keys()) {
7349
+ if (this.isEligibleDropContainer(id, true)) ids2.add(id);
7350
+ }
7351
+ return this.validDropContainerIds = ids2;
7352
+ }
7296
7353
  setTargetPlane(plane) {
7297
7354
  this.targetPlane = plane;
7298
7355
  }
@@ -7311,6 +7368,17 @@ var DragIntentEngine = class {
7311
7368
  isInvalidDropContainer(containerId) {
7312
7369
  return containerId === this.activeId || this.isContainerDescendantOf(containerId, this.activeId);
7313
7370
  }
7371
+ /**
7372
+ * The full "can this container accept the active node this drag" predicate.
7373
+ * SINGLE definition — every caller routes through it so no two parts of the
7374
+ * system can disagree about which containers are droppable.
7375
+ */
7376
+ isEligibleDropContainer(containerId, laneLock) {
7377
+ if (this.isInvalidDropContainer(containerId)) return false;
7378
+ if (this.isBlockedContainer(containerId)) return false;
7379
+ if (laneLock && !this.isLaneContainer(containerId)) return false;
7380
+ return this.hasUsableSlots(containerId);
7381
+ }
7314
7382
  /**
7315
7383
  * Resolve the drag intent from the current pointer position.
7316
7384
  *
@@ -7398,9 +7466,7 @@ var DragIntentEngine = class {
7398
7466
  const childNode = this.getNodeRect(childId);
7399
7467
  if (!childNode) continue;
7400
7468
  if (!pointInRect(px, py, childNode)) continue;
7401
- const relX = (px - childNode.left) / childNode.width;
7402
- const relY = (py - childNode.top) / childNode.height;
7403
- if (relX > NEST_EDGE_FRACTION && relX < 1 - NEST_EDGE_FRACTION && relY > NEST_EDGE_FRACTION && relY < 1 - NEST_EDGE_FRACTION) {
7469
+ if (isInNestZone(px, py, childNode)) {
7404
7470
  return {
7405
7471
  containerId: pointerContainerId,
7406
7472
  index: container.children.indexOf(childId),
@@ -7414,11 +7480,8 @@ var DragIntentEngine = class {
7414
7480
  resolvePointerContainer(px, py, laneLock) {
7415
7481
  const pointerContainers = [];
7416
7482
  for (const [id, node] of this.containerIndex) {
7417
- if (this.isInvalidDropContainer(id)) continue;
7418
- if (this.isBlockedContainer(id)) continue;
7419
- if (laneLock && !this.isLaneContainer(id)) continue;
7420
7483
  if (!pointInRect(px, py, node.rect)) continue;
7421
- if (this.hasUsableSlots(id) === false) continue;
7484
+ if (!this.isEligibleDropContainer(id, laneLock)) continue;
7422
7485
  const r = node.rect;
7423
7486
  const edgeDistX = Math.min(px - r.left, r.right - px);
7424
7487
  const edgeDistY = Math.min(py - r.top, r.bottom - py);
@@ -7435,11 +7498,11 @@ var DragIntentEngine = class {
7435
7498
  let pointerContainerId = pointerContainers[0]?.id ?? null;
7436
7499
  if (!pointerContainerId && this.lastIntent?.mode === "reorder") {
7437
7500
  const lastContainer = this.containerIndex.get(this.lastIntent.containerId);
7438
- 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)) {
7501
+ if (lastContainer && this.isEligibleDropContainer(lastContainer.id, laneLock) && pointInExpandedRect(px, py, lastContainer.rect, LAST_CONTAINER_REENTRY_SLOP_PX)) {
7439
7502
  pointerContainerId = lastContainer.id;
7440
7503
  }
7441
7504
  }
7442
- if (this.originContainerId && this.originContainerId !== pointerContainerId && !this.isInvalidDropContainer(this.originContainerId) && !this.isBlockedContainer(this.originContainerId) && (!laneLock || this.isLaneContainer(this.originContainerId)) && this.hasUsableSlots(this.originContainerId)) {
7505
+ if (this.originContainerId && this.originContainerId !== pointerContainerId && this.isEligibleDropContainer(this.originContainerId, laneLock)) {
7443
7506
  const originNode = this.containerIndex.get(this.originContainerId);
7444
7507
  const currentNode = pointerContainerId ? this.containerIndex.get(pointerContainerId) : null;
7445
7508
  const originIsDeeper = originNode && (!currentNode || originNode.depth >= currentNode.depth);
@@ -7852,7 +7915,8 @@ function useDragVisualLoop({
7852
7915
  refs,
7853
7916
  getCommitLaneSnapshot,
7854
7917
  setInsideRect,
7855
- setDropLine
7918
+ setDropLine,
7919
+ setActiveLaneContainerId
7856
7920
  }) {
7857
7921
  (0, import_react12.useEffect)(() => {
7858
7922
  const engine = refs.engineRef.current;
@@ -7860,6 +7924,11 @@ function useDragVisualLoop({
7860
7924
  if (!activeDrag || !engine || !snapshot) return;
7861
7925
  let rafId;
7862
7926
  const animator = refs.animatorRef.current;
7927
+ const publishActiveLane = (containerId) => {
7928
+ if (refs.activeLaneContainerIdRef.current === containerId) return;
7929
+ refs.activeLaneContainerIdRef.current = containerId;
7930
+ setActiveLaneContainerId(containerId);
7931
+ };
7863
7932
  const setTargetPlane = (nextPlane) => {
7864
7933
  const prevPlane = refs.targetPlaneRef.current;
7865
7934
  if (prevPlane === nextPlane) return;
@@ -7878,6 +7947,7 @@ function useDragVisualLoop({
7878
7947
  const safeZone = refs.safeZoneRef.current;
7879
7948
  if (isPointerBlocked(px, py, safeZone)) {
7880
7949
  setTargetPlane("lane");
7950
+ publishActiveLane(null);
7881
7951
  refs.lastIntentRef.current = null;
7882
7952
  refs.visualIntentRef.current = null;
7883
7953
  refs.nestCandidateRef.current = null;
@@ -7919,6 +7989,7 @@ function useDragVisualLoop({
7919
7989
  let intent = engine.resolve(px, py, window.scrollX, window.scrollY, snapshot.scroll.x, snapshot.scroll.y);
7920
7990
  if (!intent) {
7921
7991
  setTargetPlane("lane");
7992
+ publishActiveLane(null);
7922
7993
  refs.lastIntentRef.current = null;
7923
7994
  refs.visualIntentRef.current = null;
7924
7995
  refs.nestCandidateRef.current = null;
@@ -7944,6 +8015,7 @@ function useDragVisualLoop({
7944
8015
  const targetId = intent.slotId.split("::nest::")[1] ?? "";
7945
8016
  if (!targetId) {
7946
8017
  setTargetPlane("lane");
8018
+ publishActiveLane(null);
7947
8019
  rafId = requestAnimationFrame(tick);
7948
8020
  return;
7949
8021
  }
@@ -7965,12 +8037,14 @@ function useDragVisualLoop({
7965
8037
  };
7966
8038
  refs.visualIntentRef.current = intent;
7967
8039
  refs.lastIntentRef.current = intent;
8040
+ publishActiveLane(intent.containerId);
7968
8041
  } else {
7969
8042
  setTargetPlane("lane");
7970
8043
  refs.nestCandidateRef.current = null;
7971
8044
  refs.nestTargetRef.current = null;
7972
8045
  refs.lastIntentRef.current = intent;
7973
8046
  refs.visualIntentRef.current = intent;
8047
+ publishActiveLane(intent.containerId);
7974
8048
  setInsideRect(null);
7975
8049
  const proj = refs.projectionRef.current;
7976
8050
  const slot = proj.slotIndex.get(intent.slotId);
@@ -8277,7 +8351,8 @@ function useDragLifecycle({
8277
8351
  syncHistoryAvailability,
8278
8352
  resolveNodeElement,
8279
8353
  setInsideRect,
8280
- setDropLine
8354
+ setDropLine,
8355
+ setActiveLaneContainerId
8281
8356
  }) {
8282
8357
  const [activeDrag, setActiveDrag] = (0, import_react13.useState)(null);
8283
8358
  const clearDragState = (0, import_react13.useCallback)(() => {
@@ -8288,13 +8363,12 @@ function useDragLifecycle({
8288
8363
  refs.snapshotRef.current = null;
8289
8364
  refs.projectionRef.current = EMPTY_PROJECTION;
8290
8365
  refs.engineRef.current = null;
8291
- refs.blockedContainerIdsRef.current = /* @__PURE__ */ new Set();
8292
8366
  refs.lastIntentRef.current = null;
8293
8367
  refs.visualIntentRef.current = null;
8294
8368
  refs.sourceContainerIdRef.current = null;
8369
+ refs.activeLaneContainerIdRef.current = null;
8295
8370
  refs.rowLockedRef.current = false;
8296
8371
  refs.dragSessionAnchorRef.current = null;
8297
- refs.lockRef.current = { containerId: null, isCrossUnlocked: false, exitSince: null, unlockUntilTs: null };
8298
8372
  refs.targetPlaneRef.current = "lane";
8299
8373
  refs.nestCandidateRef.current = null;
8300
8374
  refs.nestTargetRef.current = null;
@@ -8305,6 +8379,7 @@ function useDragLifecycle({
8305
8379
  refs.safeZoneRef.current = null;
8306
8380
  setInsideRect(null);
8307
8381
  setDropLine(null);
8382
+ setActiveLaneContainerId(null);
8308
8383
  setActiveDrag(null);
8309
8384
  resume();
8310
8385
  }, [clearCommitLaneCache, resume]);
@@ -8362,7 +8437,6 @@ function useDragLifecycle({
8362
8437
  const sourceCommitLane = getCommitLaneSnapshot(srcContainerId);
8363
8438
  const startPosition = getCurrentCommitPlacement(anchorNodeId, srcContainerId);
8364
8439
  const blockedContainerIds = buildBlockedStructuralContainerIds(proj, refs.nodeMapRef.current, srcContainerId);
8365
- refs.blockedContainerIdsRef.current = blockedContainerIds;
8366
8440
  const engineOptions = buildDragIntentEngineOptions(anchorScanned.element, srcContainerId, blockedContainerIds);
8367
8441
  refs.rowLockedRef.current = !!engineOptions.laneAllowedContainers;
8368
8442
  refs.engineRef.current = createDragIntentEngine(proj, anchorNodeId, engineOptions);
@@ -8371,7 +8445,6 @@ function useDragLifecycle({
8371
8445
  refs.sourceContainerIdRef.current = srcContainerId;
8372
8446
  refs.sourceCommitLaneRef.current = sourceCommitLane;
8373
8447
  refs.dragSessionAnchorRef.current = anchor;
8374
- refs.lockRef.current = { containerId: srcContainerId, isCrossUnlocked: true, exitSince: null, unlockUntilTs: null };
8375
8448
  refs.dragStartPositionRef.current = startPosition;
8376
8449
  refs.safeZoneRef.current = resolveSafeZoneRect();
8377
8450
  pause();
@@ -8930,6 +9003,7 @@ function commitExternalDrop(x, y, engine, component, refs, resolveNodeElement, s
8930
9003
  // src/client/dnd/design-mode/DesignModeOverlay.tsx
8931
9004
  var import_jsx_runtime4 = require("react/jsx-runtime");
8932
9005
  var DROP_ANIMATION = { duration: DURATION_MS, easing: EASING };
9006
+ var EMPTY_CONTAINER_ID_SET = /* @__PURE__ */ new Set();
8933
9007
  function resizeCursorFromHandle(handle) {
8934
9008
  switch (handle) {
8935
9009
  case "n":
@@ -9217,13 +9291,9 @@ function DesignModeOverlay() {
9217
9291
  const sensors = useSensors(
9218
9292
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
9219
9293
  );
9220
- const collisionDetection = (0, import_react16.useCallback)(
9221
- (args) => createDesignModeCollisionDetection(refs.blockedContainerIdsRef.current)(args),
9222
- []
9223
- // eslint-disable-line react-hooks/exhaustive-deps -- refs are stable
9224
- );
9225
9294
  const [insideRect, setInsideRect] = (0, import_react16.useState)(null);
9226
9295
  const [dropLine, setDropLine] = (0, import_react16.useState)(null);
9296
+ const [activeLaneContainerId, setActiveLaneContainerId] = (0, import_react16.useState)(null);
9227
9297
  (0, import_react16.useEffect)(() => {
9228
9298
  if (!enabled) return;
9229
9299
  const fn = (e) => {
@@ -9259,8 +9329,43 @@ function DesignModeOverlay() {
9259
9329
  syncHistoryAvailability,
9260
9330
  resolveNodeElement,
9261
9331
  setInsideRect,
9262
- setDropLine
9332
+ setDropLine,
9333
+ setActiveLaneContainerId
9263
9334
  });
9335
+ const dropCandidateContainerIds = (0, import_react16.useMemo)(
9336
+ () => activeDrag ? refs.engineRef.current?.getValidDropContainerIds() ?? null : null,
9337
+ [activeDrag]
9338
+ // eslint-disable-line react-hooks/exhaustive-deps -- refs are stable
9339
+ );
9340
+ const visibleCandidateContainerIds = (0, import_react16.useMemo)(() => {
9341
+ if (!dropCandidateContainerIds) return null;
9342
+ const rects = refs.snapshotRef.current?.rects;
9343
+ if (!rects) return dropCandidateContainerIds;
9344
+ const vw = window.innerWidth;
9345
+ const vh = window.innerHeight;
9346
+ const visible = /* @__PURE__ */ new Set();
9347
+ for (const id of dropCandidateContainerIds) {
9348
+ const r = rects.get(id);
9349
+ if (!r) continue;
9350
+ if (r.right < 0 || r.bottom < 0 || r.left > vw || r.top > vh) continue;
9351
+ visible.add(id);
9352
+ }
9353
+ return visible;
9354
+ }, [dropCandidateContainerIds]);
9355
+ const dragGuide = (0, import_react16.useMemo)(() => {
9356
+ if (!activeDrag) return null;
9357
+ const snapshot = refs.snapshotRef.current;
9358
+ if (!snapshot) return null;
9359
+ const activeChildIds = activeLaneContainerId ? refs.engineRef.current?.getContainerChildren(activeLaneContainerId) ?? [] : [];
9360
+ return {
9361
+ rects: snapshot.rects,
9362
+ startScroll: snapshot.scroll,
9363
+ candidateContainerIds: visibleCandidateContainerIds ?? EMPTY_CONTAINER_ID_SET,
9364
+ activeContainerId: activeLaneContainerId,
9365
+ activeChildIds,
9366
+ draggingNodeId: activeDrag.nodeId
9367
+ };
9368
+ }, [activeDrag, activeLaneContainerId, visibleCandidateContainerIds]);
9264
9369
  (0, import_react16.useEffect)(() => {
9265
9370
  if (!enabled || activeDrag) return;
9266
9371
  if (undoRequestId === refs.processedUndoIdRef.current) return;
@@ -9323,7 +9428,7 @@ function DesignModeOverlay() {
9323
9428
  safeZoneRef: refs.safeZoneRef,
9324
9429
  autoScrollStateRef: refs.autoScrollStateRef
9325
9430
  });
9326
- useDragVisualLoop({ activeDrag, refs, getCommitLaneSnapshot, setInsideRect, setDropLine });
9431
+ useDragVisualLoop({ activeDrag, refs, getCommitLaneSnapshot, setInsideRect, setDropLine, setActiveLaneContainerId });
9327
9432
  useExternalDragLoop({
9328
9433
  externalDrag,
9329
9434
  refs,
@@ -9343,7 +9448,6 @@ function DesignModeOverlay() {
9343
9448
  DndContext,
9344
9449
  {
9345
9450
  sensors,
9346
- collisionDetection,
9347
9451
  autoScroll: false,
9348
9452
  onDragStart: handleDragStart,
9349
9453
  onDragEnd: handleDragEnd,
@@ -9352,10 +9456,8 @@ function DesignModeOverlay() {
9352
9456
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
9353
9457
  ElementHighlight,
9354
9458
  {
9355
- elements,
9356
- draggingNodeId: activeDrag?.nodeId ?? null,
9357
- activeEl: refs.activeElRef.current,
9358
- arrangeSelectedNodeId,
9459
+ selected: arrangeSelectedNodeId ? nodeMap.get(arrangeSelectedNodeId) ?? null : null,
9460
+ dragGuide,
9359
9461
  externalDragActive: externalDrag !== null,
9360
9462
  onResizeHandlePointerDown: handleResizeHandlePointerDown
9361
9463
  }