dnd-block-tree 0.4.0 → 1.0.0

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.
package/dist/index.js CHANGED
@@ -15,11 +15,11 @@ function extractBlockId(zoneId) {
15
15
  }
16
16
 
17
17
  // src/core/collision.ts
18
- function computeCollisionScores(droppableContainers, collisionRect) {
18
+ function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
19
19
  const pointerX = collisionRect.left + collisionRect.width / 2;
20
20
  const pointerY = collisionRect.top + collisionRect.height / 2;
21
21
  const candidates = droppableContainers.map((container) => {
22
- const rect = container.rect.current;
22
+ const rect = snapshotRects?.get(container.id) ?? container.rect.current;
23
23
  if (!rect) return null;
24
24
  const distanceToTop = Math.abs(pointerY - rect.top);
25
25
  const distanceToBottom = Math.abs(pointerY - rect.bottom);
@@ -29,16 +29,17 @@ function computeCollisionScores(droppableContainers, collisionRect) {
29
29
  const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
30
30
  let horizontalScore = 0;
31
31
  if (isWithinX) {
32
- horizontalScore = -rect.left * 0.1;
32
+ horizontalScore = Math.abs(pointerX - rect.left) * 0.3;
33
33
  } else {
34
34
  const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
35
- horizontalScore = Math.min(distanceToZone, 50);
35
+ horizontalScore = distanceToZone * 2;
36
36
  }
37
37
  return {
38
38
  id: container.id,
39
39
  data: {
40
40
  droppableContainer: container,
41
- value: edgeDistance + bias + horizontalScore
41
+ value: edgeDistance + bias + horizontalScore,
42
+ left: rect.left
42
43
  }
43
44
  };
44
45
  }).filter((c) => c !== null);
@@ -57,14 +58,14 @@ var weightedVerticalCollision = ({
57
58
  const candidates = computeCollisionScores(droppableContainers, collisionRect);
58
59
  return candidates.slice(0, 1);
59
60
  };
60
- function createStickyCollision(threshold = 15) {
61
+ function createStickyCollision(threshold = 15, snapshotRef) {
61
62
  let currentZoneId = null;
62
63
  const detector = ({
63
64
  droppableContainers,
64
65
  collisionRect
65
66
  }) => {
66
67
  if (!collisionRect) return [];
67
- const candidates = computeCollisionScores(droppableContainers, collisionRect);
68
+ const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
68
69
  if (candidates.length === 0) return [];
69
70
  const bestCandidate = candidates[0];
70
71
  const bestScore = bestCandidate.data.value;
@@ -72,7 +73,11 @@ function createStickyCollision(threshold = 15) {
72
73
  const currentCandidate = candidates.find((c) => c.id === currentZoneId);
73
74
  if (currentCandidate) {
74
75
  const currentScore = currentCandidate.data.value;
75
- if (currentScore - bestScore < threshold) {
76
+ const currentLeft = currentCandidate.data.left;
77
+ const bestLeft = bestCandidate.data.left;
78
+ const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
79
+ const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
80
+ if (currentScore - bestScore < effectiveThreshold) {
76
81
  return [currentCandidate];
77
82
  }
78
83
  }
@@ -135,7 +140,7 @@ function useConfiguredSensors(config = {}) {
135
140
  distance: activationDistance
136
141
  };
137
142
  touchConstraint = {
138
- delay: 200,
143
+ delay: config.longPressDelay ?? 200,
139
144
  tolerance: 5
140
145
  };
141
146
  }
@@ -197,6 +202,11 @@ function debounce(fn, delay) {
197
202
  function generateId() {
198
203
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
199
204
  }
205
+ function triggerHaptic(durationMs = 10) {
206
+ if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
207
+ navigator.vibrate(durationMs);
208
+ }
209
+ }
200
210
  function DropZoneComponent({
201
211
  id,
202
212
  parentId,
@@ -229,6 +239,9 @@ function DropZoneComponent({
229
239
  );
230
240
  }
231
241
  var DropZone = react.memo(DropZoneComponent);
242
+ function GhostPreview({ children }) {
243
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-dnd-ghost": true, className: "opacity-50", style: { pointerEvents: "none" }, children });
244
+ }
232
245
  function DraggableBlock({
233
246
  block,
234
247
  children,
@@ -278,10 +291,15 @@ function TreeRenderer({
278
291
  draggedBlock,
279
292
  focusedId,
280
293
  selectedIds,
281
- onBlockClick
294
+ onBlockClick,
295
+ animation,
296
+ virtualVisibleIds
282
297
  }) {
283
298
  const items = blocksByParent.get(parentId) ?? [];
284
- const filteredBlocks = items.filter((block) => block.id !== activeId);
299
+ let filteredBlocks = items.filter((block) => block.id !== activeId);
300
+ if (virtualVisibleIds && depth === 0) {
301
+ filteredBlocks = filteredBlocks.filter((block) => virtualVisibleIds.has(block.id));
302
+ }
285
303
  const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
286
304
  const containerClass = depth === 0 ? rootClassName : indentClassName;
287
305
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, style: { minWidth: 0 }, children: [
@@ -310,7 +328,7 @@ function TreeRenderer({
310
328
  }
311
329
  const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
312
330
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
313
- ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
331
+ ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
314
332
  block: draggedBlock,
315
333
  isDragging: true,
316
334
  depth
@@ -325,7 +343,11 @@ function TreeRenderer({
325
343
  onBlockClick,
326
344
  children: ({ isDragging }) => {
327
345
  if (isContainer) {
328
- const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
346
+ const expandStyle = animation?.expandDuration ? {
347
+ transition: `opacity ${animation.expandDuration}ms ${animation.easing ?? "ease"}`,
348
+ opacity: isExpanded ? 1 : 0
349
+ } : void 0;
350
+ const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: expandStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
329
351
  TreeRenderer,
330
352
  {
331
353
  blocks,
@@ -347,7 +369,9 @@ function TreeRenderer({
347
369
  draggedBlock,
348
370
  focusedId,
349
371
  selectedIds,
350
- onBlockClick
372
+ onBlockClick,
373
+ animation,
374
+ virtualVisibleIds
351
375
  }
352
376
  ) }) : null;
353
377
  return Renderer({
@@ -382,7 +406,7 @@ function TreeRenderer({
382
406
  }),
383
407
  showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
384
408
  const GhostRenderer = renderers[draggedBlock.type];
385
- return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
409
+ return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
386
410
  block: draggedBlock,
387
411
  isDragging: true,
388
412
  depth
@@ -634,6 +658,23 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
634
658
  }
635
659
  if (dragged.id === zoneTargetId) return state;
636
660
  const oldList = byParent.get(oldParentId) ?? [];
661
+ const currentIndexInOldParent = oldList.indexOf(dragged.id);
662
+ const preNewList = byParent.get(newParentId) ?? [];
663
+ let targetIndex;
664
+ if (isInto) {
665
+ targetIndex = 0;
666
+ } else if (isEnd) {
667
+ targetIndex = preNewList.length;
668
+ } else {
669
+ const idx = preNewList.indexOf(zoneTargetId);
670
+ targetIndex = idx === -1 ? preNewList.length : isAfter ? idx + 1 : idx;
671
+ }
672
+ if (oldParentId === newParentId && currentIndexInOldParent !== -1) {
673
+ const adjustedTarget = targetIndex > currentIndexInOldParent ? targetIndex - 1 : targetIndex;
674
+ if (adjustedTarget === currentIndexInOldParent) {
675
+ return state;
676
+ }
677
+ }
637
678
  const filtered = oldList.filter((id) => id !== dragged.id);
638
679
  byParent.set(oldParentId, filtered);
639
680
  const newList = [...byParent.get(newParentId) ?? []];
@@ -646,10 +687,6 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
646
687
  const idx = newList.indexOf(zoneTargetId);
647
688
  insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
648
689
  }
649
- const currentIndex = newList.indexOf(dragged.id);
650
- if (dragged.parentId === newParentId && currentIndex === insertIndex) {
651
- return state;
652
- }
653
690
  newList.splice(insertIndex, 0, dragged.id);
654
691
  byParent.set(newParentId, newList);
655
692
  let newOrder = dragged.order;
@@ -786,18 +823,21 @@ function BlockTree({
786
823
  canDrop,
787
824
  collisionDetection,
788
825
  sensors: sensorConfig,
826
+ animation,
789
827
  initialExpanded,
790
828
  orderingStrategy = "integer",
791
829
  maxDepth,
792
830
  keyboardNavigation = false,
793
831
  multiSelect = false,
794
832
  selectedIds: externalSelectedIds,
795
- onSelectionChange
833
+ onSelectionChange,
834
+ virtualize
796
835
  }) {
797
836
  const sensors = useConfiguredSensors({
798
837
  activationDistance: sensorConfig?.activationDistance ?? activationDistance,
799
838
  activationDelay: sensorConfig?.activationDelay,
800
- tolerance: sensorConfig?.tolerance
839
+ tolerance: sensorConfig?.tolerance,
840
+ longPressDelay: sensorConfig?.longPressDelay
801
841
  });
802
842
  const initialExpandedMap = react.useMemo(
803
843
  () => computeInitialExpanded(blocks, containerTypes, initialExpanded),
@@ -816,7 +856,20 @@ function BlockTree({
816
856
  const cachedReorderRef = react.useRef(null);
817
857
  const fromPositionRef = react.useRef(null);
818
858
  const draggedIdsRef = react.useRef([]);
819
- const stickyCollisionRef = react.useRef(createStickyCollision(20));
859
+ const snapshotRectsRef = react.useRef(null);
860
+ const needsResnapshot = react.useRef(false);
861
+ const stickyCollisionRef = react.useRef(createStickyCollision(20, snapshotRectsRef));
862
+ const snapshotZoneRects = react.useCallback(() => {
863
+ const root = rootRef.current;
864
+ if (!root) return;
865
+ const zones = root.querySelectorAll("[data-zone-id]");
866
+ const map = /* @__PURE__ */ new Map();
867
+ zones.forEach((el) => {
868
+ const id = el.getAttribute("data-zone-id");
869
+ if (id) map.set(id, el.getBoundingClientRect());
870
+ });
871
+ snapshotRectsRef.current = map;
872
+ }, []);
820
873
  const [, forceRender] = react.useReducer((x) => x + 1, 0);
821
874
  const debouncedSetVirtual = react.useRef(
822
875
  debounce((newBlocks) => {
@@ -825,6 +878,7 @@ function BlockTree({
825
878
  } else {
826
879
  stateRef.current.virtualState = null;
827
880
  }
881
+ needsResnapshot.current = true;
828
882
  forceRender();
829
883
  }, previewDebounce)
830
884
  ).current;
@@ -963,6 +1017,13 @@ function BlockTree({
963
1017
  }
964
1018
  lastClickedIdRef.current = blockId;
965
1019
  }, [multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1020
+ react.useEffect(() => {
1021
+ if (!needsResnapshot.current || !stateRef.current.isDragging) return;
1022
+ needsResnapshot.current = false;
1023
+ requestAnimationFrame(() => {
1024
+ snapshotZoneRects();
1025
+ });
1026
+ });
966
1027
  react.useEffect(() => {
967
1028
  if (!keyboardNavigation || !focusedIdRef.current || !rootRef.current) return;
968
1029
  const el = rootRef.current.querySelector(`[data-block-id="${focusedIdRef.current}"]`);
@@ -1008,12 +1069,16 @@ function BlockTree({
1008
1069
  setSelectedIds(/* @__PURE__ */ new Set([id]));
1009
1070
  }
1010
1071
  }
1072
+ if (sensorConfig?.hapticFeedback) {
1073
+ triggerHaptic();
1074
+ }
1011
1075
  stateRef.current.activeId = id;
1012
1076
  stateRef.current.isDragging = true;
1013
1077
  initialBlocksRef.current = [...blocks];
1014
1078
  cachedReorderRef.current = null;
1079
+ needsResnapshot.current = true;
1015
1080
  forceRender();
1016
- }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1081
+ }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
1017
1082
  const handleDragMove = react.useCallback((event) => {
1018
1083
  if (!onDragMove) return;
1019
1084
  const id = stateRef.current.activeId;
@@ -1087,6 +1152,7 @@ function BlockTree({
1087
1152
  cachedReorderRef.current = null;
1088
1153
  initialBlocksRef.current = [];
1089
1154
  fromPositionRef.current = null;
1155
+ snapshotRectsRef.current = null;
1090
1156
  forceRender();
1091
1157
  return;
1092
1158
  }
@@ -1126,6 +1192,7 @@ function BlockTree({
1126
1192
  initialBlocksRef.current = [];
1127
1193
  fromPositionRef.current = null;
1128
1194
  draggedIdsRef.current = [];
1195
+ snapshotRectsRef.current = null;
1129
1196
  if (cached && onChange) {
1130
1197
  onChange(cached.reorderedBlocks);
1131
1198
  }
@@ -1154,6 +1221,7 @@ function BlockTree({
1154
1221
  initialBlocksRef.current = [];
1155
1222
  fromPositionRef.current = null;
1156
1223
  draggedIdsRef.current = [];
1224
+ snapshotRectsRef.current = null;
1157
1225
  forceRender();
1158
1226
  }, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
1159
1227
  const handleHover = react.useCallback((zoneId, _parentId) => {
@@ -1207,6 +1275,61 @@ function BlockTree({
1207
1275
  forceRender();
1208
1276
  }, [blocks, onExpandChange]);
1209
1277
  toggleExpandRef.current = handleToggleExpand;
1278
+ const virtualContainerRef = react.useRef(null);
1279
+ const [virtualScroll, setVirtualScroll] = react.useState({ scrollTop: 0, clientHeight: 0 });
1280
+ react.useEffect(() => {
1281
+ if (!virtualize) return;
1282
+ const el = virtualContainerRef.current;
1283
+ if (!el) return;
1284
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1285
+ const onScroll = () => {
1286
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1287
+ };
1288
+ el.addEventListener("scroll", onScroll, { passive: true });
1289
+ return () => el.removeEventListener("scroll", onScroll);
1290
+ }, [virtualize]);
1291
+ const virtualResult = react.useMemo(() => {
1292
+ if (!virtualize) return null;
1293
+ const { itemHeight, overscan = 5 } = virtualize;
1294
+ const { scrollTop, clientHeight } = virtualScroll;
1295
+ const totalHeight = visibleBlockIds.length * itemHeight;
1296
+ const startRaw = Math.floor(scrollTop / itemHeight);
1297
+ const visibleCount = Math.ceil(clientHeight / itemHeight);
1298
+ const start = Math.max(0, startRaw - overscan);
1299
+ const end = Math.min(visibleBlockIds.length - 1, startRaw + visibleCount + overscan);
1300
+ const offsetY = start * itemHeight;
1301
+ const visibleSet = /* @__PURE__ */ new Set();
1302
+ for (let i = start; i <= end; i++) {
1303
+ visibleSet.add(visibleBlockIds[i]);
1304
+ }
1305
+ return { totalHeight, offsetY, visibleSet };
1306
+ }, [virtualize, virtualScroll, visibleBlockIds]);
1307
+ const treeContent = /* @__PURE__ */ jsxRuntime.jsx(
1308
+ TreeRenderer,
1309
+ {
1310
+ blocks,
1311
+ blocksByParent,
1312
+ parentId: null,
1313
+ activeId: stateRef.current.activeId,
1314
+ expandedMap: stateRef.current.expandedMap,
1315
+ renderers,
1316
+ containerTypes,
1317
+ onHover: handleHover,
1318
+ onToggleExpand: handleToggleExpand,
1319
+ dropZoneClassName,
1320
+ dropZoneActiveClassName,
1321
+ indentClassName,
1322
+ rootClassName: className,
1323
+ canDrag,
1324
+ previewPosition,
1325
+ draggedBlock,
1326
+ focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1327
+ selectedIds: multiSelect ? selectedIds : void 0,
1328
+ onBlockClick: multiSelect ? handleBlockClick : void 0,
1329
+ animation,
1330
+ virtualVisibleIds: virtualResult?.visibleSet ?? null
1331
+ }
1332
+ );
1210
1333
  return /* @__PURE__ */ jsxRuntime.jsxs(
1211
1334
  core.DndContext,
1212
1335
  {
@@ -1218,7 +1341,20 @@ function BlockTree({
1218
1341
  onDragEnd: handleDragEnd,
1219
1342
  onDragCancel: handleDragCancel,
1220
1343
  children: [
1221
- /* @__PURE__ */ jsxRuntime.jsx(
1344
+ virtualize ? /* @__PURE__ */ jsxRuntime.jsx(
1345
+ "div",
1346
+ {
1347
+ ref: (el) => {
1348
+ virtualContainerRef.current = el;
1349
+ rootRef.current = el;
1350
+ },
1351
+ className,
1352
+ style: { minWidth: 0, overflow: "auto", position: "relative" },
1353
+ onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1354
+ role: keyboardNavigation ? "tree" : void 0,
1355
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: virtualResult.totalHeight, position: "relative" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", top: virtualResult.offsetY, left: 0, right: 0 }, children: treeContent }) })
1356
+ }
1357
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1222
1358
  "div",
1223
1359
  {
1224
1360
  ref: rootRef,
@@ -1226,30 +1362,7 @@ function BlockTree({
1226
1362
  style: { minWidth: 0 },
1227
1363
  onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1228
1364
  role: keyboardNavigation ? "tree" : void 0,
1229
- children: /* @__PURE__ */ jsxRuntime.jsx(
1230
- TreeRenderer,
1231
- {
1232
- blocks,
1233
- blocksByParent,
1234
- parentId: null,
1235
- activeId: stateRef.current.activeId,
1236
- expandedMap: stateRef.current.expandedMap,
1237
- renderers,
1238
- containerTypes,
1239
- onHover: handleHover,
1240
- onToggleExpand: handleToggleExpand,
1241
- dropZoneClassName,
1242
- dropZoneActiveClassName,
1243
- indentClassName,
1244
- rootClassName: className,
1245
- canDrag,
1246
- previewPosition,
1247
- draggedBlock,
1248
- focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1249
- selectedIds: multiSelect ? selectedIds : void 0,
1250
- onBlockClick: multiSelect ? handleBlockClick : void 0
1251
- }
1252
- )
1365
+ children: treeContent
1253
1366
  }
1254
1367
  ),
1255
1368
  /* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { activeBlock, selectedCount: multiSelect ? selectedIds.size : 0, children: dragOverlay })
@@ -1257,6 +1370,16 @@ function BlockTree({
1257
1370
  }
1258
1371
  );
1259
1372
  }
1373
+ function BlockTreeSSR({ fallback = null, ...props }) {
1374
+ const [mounted, setMounted] = react.useState(false);
1375
+ react.useEffect(() => {
1376
+ setMounted(true);
1377
+ }, []);
1378
+ if (!mounted) {
1379
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
1380
+ }
1381
+ return /* @__PURE__ */ jsxRuntime.jsx(BlockTree, { ...props });
1382
+ }
1260
1383
  function blockReducer(state, action, containerTypes = [], orderingStrategy = "integer", maxDepth) {
1261
1384
  switch (action.type) {
1262
1385
  case "ADD_ITEM": {
@@ -1661,8 +1784,143 @@ function useBlockHistory(initialBlocks, options = {}) {
1661
1784
  canRedo: state.future.length > 0
1662
1785
  };
1663
1786
  }
1787
+ function useLayoutAnimation(containerRef, options = {}) {
1788
+ const {
1789
+ duration = 200,
1790
+ easing = "ease",
1791
+ selector = "[data-block-id]"
1792
+ } = options;
1793
+ const prevPositions = react.useRef(/* @__PURE__ */ new Map());
1794
+ react.useLayoutEffect(() => {
1795
+ const container = containerRef.current;
1796
+ if (!container) return;
1797
+ const children = container.querySelectorAll(selector);
1798
+ const currentPositions = /* @__PURE__ */ new Map();
1799
+ children.forEach((el) => {
1800
+ const id = el.dataset.blockId;
1801
+ if (id) {
1802
+ currentPositions.set(id, el.getBoundingClientRect());
1803
+ }
1804
+ });
1805
+ children.forEach((el) => {
1806
+ const htmlEl = el;
1807
+ const id = htmlEl.dataset.blockId;
1808
+ if (!id) return;
1809
+ const prev = prevPositions.current.get(id);
1810
+ const curr = currentPositions.get(id);
1811
+ if (!prev || !curr) return;
1812
+ const deltaY = prev.top - curr.top;
1813
+ const deltaX = prev.left - curr.left;
1814
+ if (deltaY === 0 && deltaX === 0) return;
1815
+ htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
1816
+ htmlEl.style.transition = "none";
1817
+ requestAnimationFrame(() => {
1818
+ htmlEl.style.transition = `transform ${duration}ms ${easing}`;
1819
+ htmlEl.style.transform = "";
1820
+ const onEnd = () => {
1821
+ htmlEl.style.transition = "";
1822
+ htmlEl.removeEventListener("transitionend", onEnd);
1823
+ };
1824
+ htmlEl.addEventListener("transitionend", onEnd);
1825
+ });
1826
+ });
1827
+ prevPositions.current = currentPositions;
1828
+ });
1829
+ }
1830
+ function useVirtualTree({
1831
+ containerRef,
1832
+ itemCount,
1833
+ itemHeight,
1834
+ overscan = 5
1835
+ }) {
1836
+ const [scrollTop, setScrollTop] = react.useState(0);
1837
+ const [containerHeight, setContainerHeight] = react.useState(0);
1838
+ const rafId = react.useRef(0);
1839
+ const handleScroll = react.useCallback(() => {
1840
+ cancelAnimationFrame(rafId.current);
1841
+ rafId.current = requestAnimationFrame(() => {
1842
+ const el = containerRef.current;
1843
+ if (el) {
1844
+ setScrollTop(el.scrollTop);
1845
+ setContainerHeight(el.clientHeight);
1846
+ }
1847
+ });
1848
+ }, [containerRef]);
1849
+ react.useEffect(() => {
1850
+ const el = containerRef.current;
1851
+ if (!el) return;
1852
+ setScrollTop(el.scrollTop);
1853
+ setContainerHeight(el.clientHeight);
1854
+ el.addEventListener("scroll", handleScroll, { passive: true });
1855
+ return () => {
1856
+ el.removeEventListener("scroll", handleScroll);
1857
+ cancelAnimationFrame(rafId.current);
1858
+ };
1859
+ }, [containerRef, handleScroll]);
1860
+ const totalHeight = itemCount * itemHeight;
1861
+ const startRaw = Math.floor(scrollTop / itemHeight);
1862
+ const visibleCount = Math.ceil(containerHeight / itemHeight);
1863
+ const start = Math.max(0, startRaw - overscan);
1864
+ const end = Math.min(itemCount - 1, startRaw + visibleCount + overscan);
1865
+ const offsetY = start * itemHeight;
1866
+ return {
1867
+ visibleRange: { start, end },
1868
+ totalHeight,
1869
+ offsetY
1870
+ };
1871
+ }
1872
+
1873
+ // src/utils/serialization.ts
1874
+ function flatToNested(blocks) {
1875
+ const byParent = /* @__PURE__ */ new Map();
1876
+ for (const block of blocks) {
1877
+ const key = block.parentId ?? null;
1878
+ const list = byParent.get(key);
1879
+ if (list) {
1880
+ list.push(block);
1881
+ } else {
1882
+ byParent.set(key, [block]);
1883
+ }
1884
+ }
1885
+ for (const list of byParent.values()) {
1886
+ list.sort((a, b) => {
1887
+ if (typeof a.order === "string" && typeof b.order === "string") {
1888
+ return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
1889
+ }
1890
+ return Number(a.order) - Number(b.order);
1891
+ });
1892
+ }
1893
+ function buildChildren(parentId) {
1894
+ const siblings = byParent.get(parentId) ?? [];
1895
+ return siblings.map((block) => {
1896
+ const { parentId: _p, order: _o, ...rest } = block;
1897
+ return {
1898
+ ...rest,
1899
+ children: buildChildren(block.id)
1900
+ };
1901
+ });
1902
+ }
1903
+ return buildChildren(null);
1904
+ }
1905
+ function nestedToFlat(nested) {
1906
+ const result = [];
1907
+ function walk(nodes, parentId) {
1908
+ for (let i = 0; i < nodes.length; i++) {
1909
+ const { children, ...rest } = nodes[i];
1910
+ result.push({
1911
+ ...rest,
1912
+ parentId,
1913
+ order: i
1914
+ });
1915
+ walk(children, rest.id);
1916
+ }
1917
+ }
1918
+ walk(nested, null);
1919
+ return result;
1920
+ }
1664
1921
 
1665
1922
  exports.BlockTree = BlockTree;
1923
+ exports.BlockTreeSSR = BlockTreeSSR;
1666
1924
  exports.DragOverlay = DragOverlay;
1667
1925
  exports.DropZone = DropZone;
1668
1926
  exports.TreeRenderer = TreeRenderer;
@@ -1679,6 +1937,7 @@ exports.debounce = debounce;
1679
1937
  exports.deleteBlockAndDescendants = deleteBlockAndDescendants;
1680
1938
  exports.extractBlockId = extractBlockId;
1681
1939
  exports.extractUUID = extractUUID;
1940
+ exports.flatToNested = flatToNested;
1682
1941
  exports.generateId = generateId;
1683
1942
  exports.generateInitialKeys = generateInitialKeys;
1684
1943
  exports.generateKeyBetween = generateKeyBetween;
@@ -1689,10 +1948,14 @@ exports.getDropZoneType = getDropZoneType;
1689
1948
  exports.getSensorConfig = getSensorConfig;
1690
1949
  exports.getSubtreeDepth = getSubtreeDepth;
1691
1950
  exports.initFractionalOrder = initFractionalOrder;
1951
+ exports.nestedToFlat = nestedToFlat;
1692
1952
  exports.reparentBlockIndex = reparentBlockIndex;
1693
1953
  exports.reparentMultipleBlocks = reparentMultipleBlocks;
1954
+ exports.triggerHaptic = triggerHaptic;
1694
1955
  exports.useBlockHistory = useBlockHistory;
1695
1956
  exports.useConfiguredSensors = useConfiguredSensors;
1957
+ exports.useLayoutAnimation = useLayoutAnimation;
1958
+ exports.useVirtualTree = useVirtualTree;
1696
1959
  exports.weightedVerticalCollision = weightedVerticalCollision;
1697
1960
  //# sourceMappingURL=index.js.map
1698
1961
  //# sourceMappingURL=index.js.map