dnd-block-tree 1.2.0 → 2.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
@@ -1,2765 +1,14 @@
1
1
  'use strict';
2
2
 
3
- var core = require('@dnd-kit/core');
4
- var react = require('react');
5
- var jsxRuntime = require('react/jsx-runtime');
3
+ var react = require('@dnd-block-tree/react');
6
4
 
7
- // src/core/types.ts
8
- function getDropZoneType(zoneId) {
9
- if (zoneId.startsWith("before-")) return "before";
10
- if (zoneId.startsWith("into-")) return "into";
11
- return "after";
12
- }
13
- function extractBlockId(zoneId) {
14
- return zoneId.replace(/^(before|after|into)-/, "");
15
- }
16
5
 
17
- // src/core/collision.ts
18
- function collisionValue(d) {
19
- return d.data.value;
20
- }
21
- function collisionLeft(d) {
22
- return d.data.left;
23
- }
24
- function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
25
- const pointerX = collisionRect.left + collisionRect.width / 2;
26
- const pointerY = collisionRect.top + collisionRect.height / 2;
27
- const candidates = droppableContainers.map((container) => {
28
- const rect = snapshotRects?.get(container.id) ?? container.rect.current;
29
- if (!rect) return null;
30
- const distanceToTop = Math.abs(pointerY - rect.top);
31
- const distanceToBottom = Math.abs(pointerY - rect.bottom);
32
- const edgeDistance = Math.min(distanceToTop, distanceToBottom);
33
- const isBelowCenter = pointerY > rect.top + rect.height / 2;
34
- const bias = isBelowCenter ? -5 : 0;
35
- const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
36
- let horizontalScore = 0;
37
- if (isWithinX) {
38
- horizontalScore = Math.abs(pointerX - rect.left) * 0.3;
39
- } else {
40
- const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
41
- horizontalScore = distanceToZone * 2;
42
- }
43
- return {
44
- id: container.id,
45
- data: {
46
- droppableContainer: container,
47
- value: edgeDistance + bias + horizontalScore,
48
- left: rect.left
49
- }
50
- };
51
- }).filter((c) => c !== null);
52
- candidates.sort((a, b) => collisionValue(a) - collisionValue(b));
53
- return candidates;
54
- }
55
- var weightedVerticalCollision = ({
56
- droppableContainers,
57
- collisionRect
58
- }) => {
59
- if (!collisionRect) return [];
60
- const candidates = computeCollisionScores(droppableContainers, collisionRect);
61
- return candidates.slice(0, 1);
62
- };
63
- function createStickyCollision(threshold = 15, snapshotRef) {
64
- let currentZoneId = null;
65
- const detector = ({
66
- droppableContainers,
67
- collisionRect
68
- }) => {
69
- if (!collisionRect) return [];
70
- const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
71
- if (candidates.length === 0) return [];
72
- const bestCandidate = candidates[0];
73
- const bestScore = collisionValue(bestCandidate);
74
- if (currentZoneId !== null) {
75
- const currentCandidate = candidates.find((c) => c.id === currentZoneId);
76
- if (currentCandidate) {
77
- const currentScore = collisionValue(currentCandidate);
78
- const currentLeft = collisionLeft(currentCandidate);
79
- const bestLeft = collisionLeft(bestCandidate);
80
- const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
81
- const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
82
- if (currentScore - bestScore < effectiveThreshold) {
83
- return [currentCandidate];
84
- }
85
- }
86
- }
87
- currentZoneId = bestCandidate.id;
88
- return [bestCandidate];
89
- };
90
- detector.reset = () => {
91
- currentZoneId = null;
92
- };
93
- return detector;
94
- }
95
- var closestCenterCollision = ({
96
- droppableContainers,
97
- collisionRect
98
- }) => {
99
- if (!collisionRect) return [];
100
- const centerY = collisionRect.top + collisionRect.height / 2;
101
- const centerX = collisionRect.left + collisionRect.width / 2;
102
- const candidates = droppableContainers.map((container) => {
103
- const rect = container.rect.current;
104
- if (!rect) return null;
105
- const containerCenterX = rect.left + rect.width / 2;
106
- const containerCenterY = rect.top + rect.height / 2;
107
- const distance = Math.sqrt(
108
- Math.pow(centerX - containerCenterX, 2) + Math.pow(centerY - containerCenterY, 2)
109
- );
110
- return {
111
- id: container.id,
112
- data: {
113
- droppableContainer: container,
114
- value: distance
115
- }
116
- };
117
- }).filter((c) => c !== null);
118
- candidates.sort((a, b) => collisionValue(a) - collisionValue(b));
119
- return candidates.slice(0, 1);
120
- };
121
- var DEFAULT_ACTIVATION_DISTANCE = 8;
122
- function useConfiguredSensors(config = {}) {
123
- const {
124
- activationDistance = DEFAULT_ACTIVATION_DISTANCE,
125
- activationDelay,
126
- tolerance
127
- } = config;
128
- let pointerConstraint;
129
- let touchConstraint;
130
- if (activationDelay !== void 0) {
131
- pointerConstraint = {
132
- delay: activationDelay,
133
- tolerance: tolerance ?? 5
134
- };
135
- touchConstraint = pointerConstraint;
136
- } else {
137
- pointerConstraint = {
138
- distance: activationDistance
139
- };
140
- touchConstraint = {
141
- delay: config.longPressDelay ?? 200,
142
- tolerance: 5
143
- };
144
- }
145
- return core.useSensors(
146
- core.useSensor(core.PointerSensor, {
147
- activationConstraint: pointerConstraint
148
- }),
149
- core.useSensor(core.TouchSensor, {
150
- activationConstraint: touchConstraint
151
- }),
152
- core.useSensor(core.KeyboardSensor)
153
- );
154
- }
155
- function getSensorConfig(config = {}) {
156
- const {
157
- activationDistance = DEFAULT_ACTIVATION_DISTANCE,
158
- activationDelay,
159
- tolerance
160
- } = config;
161
- let activationConstraint;
162
- if (activationDelay !== void 0) {
163
- activationConstraint = {
164
- delay: activationDelay,
165
- tolerance: tolerance ?? 5
166
- };
167
- } else {
168
- activationConstraint = {
169
- distance: activationDistance
170
- };
171
- }
172
- return {
173
- pointer: { activationConstraint },
174
- touch: { activationConstraint }
175
- };
176
- }
177
6
 
178
- // src/utils/helper.ts
179
- function extractUUID(id, pattern = "^(before|after|into|end)-") {
180
- const regex = new RegExp(pattern);
181
- return id.replace(regex, "");
182
- }
183
- function debounce(fn, delay) {
184
- let timeoutId = null;
185
- const debounced = ((...args) => {
186
- if (timeoutId) clearTimeout(timeoutId);
187
- timeoutId = setTimeout(() => {
188
- fn(...args);
189
- timeoutId = null;
190
- }, delay);
191
- });
192
- debounced.cancel = () => {
193
- if (timeoutId) {
194
- clearTimeout(timeoutId);
195
- timeoutId = null;
196
- }
197
- };
198
- return debounced;
199
- }
200
- function generateId() {
201
- return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
202
- }
203
- function triggerHaptic(durationMs = 10) {
204
- if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
205
- navigator.vibrate(durationMs);
206
- }
207
- }
208
- function DropZoneComponent({
209
- id,
210
- parentId,
211
- onHover,
212
- activeId,
213
- className = "h-1 rounded transition-colors",
214
- activeClassName = "bg-blue-500",
215
- height = 4
216
- }) {
217
- const { setNodeRef, isOver, active } = core.useDroppable({ id });
218
- const handleInternalHover = react.useCallback(() => {
219
- onHover(id, parentId);
220
- }, [onHover, id, parentId]);
221
- react.useEffect(() => {
222
- if (isOver) handleInternalHover();
223
- }, [isOver, handleInternalHover]);
224
- const zoneBlockId = extractUUID(id);
225
- const isIntoZone = id.startsWith("into-");
226
- if (isIntoZone && active?.id && zoneBlockId === String(active.id)) return null;
227
- if (isIntoZone && activeId && zoneBlockId === activeId) return null;
228
- return /* @__PURE__ */ jsxRuntime.jsx(
229
- "div",
230
- {
231
- ref: setNodeRef,
232
- "data-zone-id": id,
233
- "data-parent-id": parentId ?? "",
234
- style: { height: isOver ? height * 2 : height },
235
- className: `${className} ${isOver ? activeClassName : "bg-transparent"}`
236
- }
237
- );
238
- }
239
- var DropZone = react.memo(DropZoneComponent);
240
- function GhostPreview({ children }) {
241
- return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-dnd-ghost": true, className: "opacity-50", style: { pointerEvents: "none" }, children });
242
- }
243
- function DraggableBlock({
244
- block,
245
- children,
246
- disabled,
247
- focusedId,
248
- isSelected,
249
- onBlockClick,
250
- isContainer,
251
- isExpanded,
252
- depth,
253
- posInSet,
254
- setSize
255
- }) {
256
- const { attributes, listeners, setNodeRef, isDragging } = core.useDraggable({
257
- id: block.id,
258
- disabled
259
- });
260
- const isFocused = focusedId === block.id;
261
- return /* @__PURE__ */ jsxRuntime.jsx(
262
- "div",
263
- {
264
- ref: setNodeRef,
265
- ...attributes,
266
- ...listeners,
267
- "data-block-id": block.id,
268
- tabIndex: isFocused ? 0 : -1,
269
- onClick: onBlockClick ? (e) => onBlockClick(block.id, e) : void 0,
270
- "data-selected": isSelected || void 0,
271
- style: { touchAction: "none", minWidth: 0, outline: "none" },
272
- role: "treeitem",
273
- "aria-level": depth + 1,
274
- "aria-posinset": posInSet,
275
- "aria-setsize": setSize,
276
- "aria-expanded": isContainer ? isExpanded : void 0,
277
- "aria-selected": isSelected ?? void 0,
278
- children: children({ isDragging })
279
- }
280
- );
281
- }
282
- function TreeRendererInner({
283
- blocks,
284
- blocksByParent,
285
- parentId,
286
- activeId,
287
- expandedMap,
288
- renderers,
289
- containerTypes,
290
- onHover,
291
- onToggleExpand,
292
- depth = 0,
293
- dropZoneClassName,
294
- dropZoneActiveClassName,
295
- indentClassName = "ml-6 border-l border-gray-200 pl-4",
296
- rootClassName = "flex flex-col gap-1",
297
- canDrag,
298
- previewPosition,
299
- draggedBlock,
300
- focusedId,
301
- selectedIds,
302
- onBlockClick,
303
- animation,
304
- virtualVisibleIds
305
- }) {
306
- const items = blocksByParent.get(parentId) ?? [];
307
- let filteredBlocks = items.filter((block) => block.id !== activeId);
308
- if (virtualVisibleIds && depth === 0) {
309
- filteredBlocks = filteredBlocks.filter((block) => virtualVisibleIds.has(block.id));
310
- }
311
- const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
312
- const containerClass = depth === 0 ? rootClassName : indentClassName;
313
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, style: { minWidth: 0 }, children: [
314
- /* @__PURE__ */ jsxRuntime.jsx(
315
- DropZone,
316
- {
317
- id: parentId ? `into-${parentId}` : "root-start",
318
- parentId,
319
- onHover,
320
- activeId,
321
- className: dropZoneClassName,
322
- activeClassName: dropZoneActiveClassName
323
- }
324
- ),
325
- filteredBlocks.map((block, index) => {
326
- const isContainer = containerTypes.includes(block.type);
327
- const isExpanded = expandedMap[block.id] !== false;
328
- const Renderer = renderers[block.type];
329
- const isDragDisabled = canDrag ? !canDrag(block) : false;
330
- const ghostBeforeThis = showGhostHere && previewPosition.index === index;
331
- const originalIndex = items.findIndex((b) => b.id === block.id);
332
- const isLastInOriginal = originalIndex === items.length - 1;
333
- if (!Renderer) {
334
- console.warn(`No renderer found for block type: ${block.type}`);
335
- return null;
336
- }
337
- const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
338
- return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
339
- ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
340
- block: draggedBlock,
341
- isDragging: true,
342
- depth
343
- }) }),
344
- /* @__PURE__ */ jsxRuntime.jsx(
345
- DraggableBlock,
346
- {
347
- block,
348
- disabled: isDragDisabled,
349
- focusedId,
350
- isSelected: selectedIds?.has(block.id),
351
- onBlockClick,
352
- isContainer,
353
- isExpanded,
354
- depth,
355
- posInSet: originalIndex + 1,
356
- setSize: items.length,
357
- children: ({ isDragging }) => {
358
- if (isContainer) {
359
- const expandStyle = animation?.expandDuration ? {
360
- transition: `opacity ${animation.expandDuration}ms ${animation.easing ?? "ease"}`,
361
- opacity: isExpanded ? 1 : 0
362
- } : void 0;
363
- const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: expandStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
364
- TreeRenderer,
365
- {
366
- blocks,
367
- blocksByParent,
368
- parentId: block.id,
369
- activeId,
370
- expandedMap,
371
- renderers,
372
- containerTypes,
373
- onHover,
374
- onToggleExpand,
375
- depth: depth + 1,
376
- dropZoneClassName,
377
- dropZoneActiveClassName,
378
- indentClassName,
379
- rootClassName,
380
- canDrag,
381
- previewPosition,
382
- draggedBlock,
383
- focusedId,
384
- selectedIds,
385
- onBlockClick,
386
- animation,
387
- virtualVisibleIds
388
- }
389
- ) }) : null;
390
- return Renderer({
391
- block,
392
- children: childContent,
393
- isDragging,
394
- depth,
395
- isExpanded,
396
- onToggleExpand: () => onToggleExpand(block.id)
397
- });
398
- }
399
- return Renderer({
400
- block,
401
- isDragging,
402
- depth
403
- });
404
- }
405
- }
406
- ),
407
- !isLastInOriginal && /* @__PURE__ */ jsxRuntime.jsx(
408
- DropZone,
409
- {
410
- id: `after-${block.id}`,
411
- parentId: block.parentId,
412
- onHover,
413
- activeId,
414
- className: dropZoneClassName,
415
- activeClassName: dropZoneActiveClassName
416
- }
417
- )
418
- ] }, block.id);
419
- }),
420
- showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
421
- const GhostRenderer = renderers[draggedBlock.type];
422
- return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
423
- block: draggedBlock,
424
- isDragging: true,
425
- depth
426
- }) }) : null;
427
- })(),
428
- /* @__PURE__ */ jsxRuntime.jsx(
429
- DropZone,
430
- {
431
- id: parentId ? `end-${parentId}` : "root-end",
432
- parentId,
433
- onHover,
434
- activeId,
435
- className: dropZoneClassName,
436
- activeClassName: dropZoneActiveClassName
437
- }
438
- )
439
- ] });
440
- }
441
- var TreeRenderer = react.memo(TreeRendererInner);
442
- function DragOverlay({
443
- activeBlock,
444
- children,
445
- selectedCount = 0
446
- }) {
447
- const showBadge = selectedCount > 1;
448
- return /* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { children: activeBlock && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
449
- showBadge && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
450
- /* @__PURE__ */ jsxRuntime.jsx(
451
- "div",
452
- {
453
- style: {
454
- position: "absolute",
455
- top: 4,
456
- left: 4,
457
- right: -4,
458
- bottom: -4,
459
- borderRadius: 8,
460
- border: "1px solid #d1d5db",
461
- background: "#f3f4f6",
462
- opacity: 0.6,
463
- zIndex: -1
464
- }
465
- }
466
- ),
467
- /* @__PURE__ */ jsxRuntime.jsx(
468
- "div",
469
- {
470
- style: {
471
- position: "absolute",
472
- top: -8,
473
- right: -8,
474
- background: "#3b82f6",
475
- color: "white",
476
- borderRadius: "50%",
477
- width: 22,
478
- height: 22,
479
- display: "flex",
480
- alignItems: "center",
481
- justifyContent: "center",
482
- fontSize: 11,
483
- fontWeight: 700,
484
- zIndex: 10,
485
- boxShadow: "0 1px 3px rgba(0,0,0,0.2)"
486
- },
487
- children: selectedCount
488
- }
489
- )
490
- ] }),
491
- children ? children(activeBlock) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white border border-gray-300 shadow-md rounded-md p-3 text-sm w-64 pointer-events-none", children: [
492
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500 uppercase text-xs tracking-wide mb-1", children: activeBlock.type }),
493
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "font-semibold text-gray-800", children: [
494
- "Block ",
495
- activeBlock.id.slice(0, 8)
496
- ] })
497
- ] })
498
- ] }) });
499
- }
500
-
501
- // src/utils/fractional.ts
502
- var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
503
- var BASE = ALPHABET.length;
504
- var FLOOR = ALPHABET[0];
505
- var MID_IDX = Math.floor(BASE / 2);
506
- var MID_CHAR = ALPHABET[MID_IDX];
507
- function charToIdx(c) {
508
- const n = ALPHABET.indexOf(c);
509
- if (n === -1) throw new Error(`Invalid fractional key character: "${c}"`);
510
- return n;
511
- }
512
- function computeMidpoint(lo, hi) {
513
- const len = Math.max(lo.length, hi.length) + 1;
514
- const loD = Array.from(lo.padEnd(len, FLOOR)).map(charToIdx);
515
- const hiD = Array.from(hi.padEnd(len, FLOOR)).map(charToIdx);
516
- const sum = new Array(len + 1).fill(0);
517
- let carry = 0;
518
- for (let i = len - 1; i >= 0; i--) {
519
- const s = loD[i] + hiD[i] + carry;
520
- sum[i + 1] = s % BASE;
521
- carry = Math.floor(s / BASE);
522
- }
523
- sum[0] = carry;
524
- const mid = new Array(len + 1).fill(0);
525
- let rem = 0;
526
- for (let i = 0; i <= len; i++) {
527
- const val = rem * BASE + sum[i];
528
- mid[i] = Math.floor(val / 2);
529
- rem = val % 2;
530
- }
531
- const digits = mid.slice(1);
532
- let end = digits.length;
533
- while (end > 1 && digits[end - 1] === 0) end--;
534
- return digits.slice(0, end).map((n) => ALPHABET[n]).join("");
535
- }
536
- function keyBefore(hi) {
537
- const firstIdx = charToIdx(hi[0]);
538
- if (firstIdx > 1) {
539
- return ALPHABET[Math.floor(firstIdx / 2)];
540
- }
541
- if (hi.length === 1) {
542
- return FLOOR + MID_CHAR;
543
- }
544
- return FLOOR + keyBefore(hi.slice(1));
545
- }
546
- function generateKeyBetween(lo, hi) {
547
- if (lo !== null && hi !== null) {
548
- if (lo >= hi) throw new Error(`lo must be strictly less than hi: "${lo}" >= "${hi}"`);
549
- return computeMidpoint(lo, hi);
550
- }
551
- if (lo === null && hi === null) return MID_CHAR;
552
- if (lo === null) return keyBefore(hi);
553
- return lo + MID_CHAR;
554
- }
555
- function generateNKeysBetween(lo, hi, n) {
556
- if (n <= 0) return [];
557
- if (n === 1) return [generateKeyBetween(lo, hi)];
558
- const keys = new Array(n);
559
- function fill(loKey, hiKey, from, to) {
560
- if (from >= to) return;
561
- const mid = Math.floor((from + to) / 2);
562
- keys[mid] = generateKeyBetween(loKey, hiKey);
563
- fill(loKey, keys[mid], from, mid);
564
- fill(keys[mid], hiKey, mid + 1, to);
565
- }
566
- fill(lo, hi, 0, n);
567
- return keys;
568
- }
569
- function generateInitialKeys(n) {
570
- return generateNKeysBetween(null, null, n);
571
- }
572
- function compareFractionalKeys(a, b) {
573
- return a < b ? -1 : a > b ? 1 : 0;
574
- }
575
- function initFractionalOrder(blocks) {
576
- const byParent = /* @__PURE__ */ new Map();
577
- for (const block of blocks) {
578
- const key = block.parentId ?? null;
579
- const list = byParent.get(key) ?? [];
580
- list.push(block);
581
- byParent.set(key, list);
582
- }
583
- const updated = /* @__PURE__ */ new Map();
584
- for (const siblings of byParent.values()) {
585
- siblings.sort((a, b) => {
586
- const ao = a.order, bo = b.order;
587
- if (typeof ao === "number" && typeof bo === "number") return ao - bo;
588
- return String(ao) < String(bo) ? -1 : String(ao) > String(bo) ? 1 : 0;
589
- });
590
- const keys = generateNKeysBetween(null, null, siblings.length);
591
- siblings.forEach((block, i) => {
592
- updated.set(block.id, { ...block, order: keys[i] });
593
- });
594
- }
595
- return blocks.map((b) => updated.get(b.id) ?? b);
596
- }
597
-
598
- // src/utils/blocks.ts
599
- function cloneMap(map) {
600
- return new Map(map);
601
- }
602
- function cloneParentMap(map) {
603
- const newMap = /* @__PURE__ */ new Map();
604
- for (const [k, v] of map.entries()) {
605
- newMap.set(k, [...v]);
606
- }
607
- return newMap;
608
- }
609
- function computeNormalizedIndex(blocks, orderingStrategy = "integer") {
610
- const byId = /* @__PURE__ */ new Map();
611
- const byParent = /* @__PURE__ */ new Map();
612
- for (const block of blocks) {
613
- byId.set(block.id, block);
614
- const key = block.parentId ?? null;
615
- const list = byParent.get(key) ?? [];
616
- byParent.set(key, [...list, block.id]);
617
- }
618
- if (orderingStrategy === "fractional") {
619
- for (const [parentId, ids] of byParent.entries()) {
620
- ids.sort((a, b) => {
621
- const orderA = String(byId.get(a).order);
622
- const orderB = String(byId.get(b).order);
623
- return compareFractionalKeys(orderA, orderB);
624
- });
625
- byParent.set(parentId, ids);
626
- }
627
- }
628
- return { byId, byParent };
629
- }
630
- function buildOrderedBlocks(index, containerTypes = [], orderingStrategy = "integer") {
631
- const result = [];
632
- const walk = (parentId) => {
633
- const children = index.byParent.get(parentId) ?? [];
634
- for (let i = 0; i < children.length; i++) {
635
- const id = children[i];
636
- const block = index.byId.get(id);
637
- if (block) {
638
- result.push(orderingStrategy === "fractional" ? block : { ...block, order: i });
639
- if (containerTypes.includes(block.type)) {
640
- walk(block.id);
641
- }
642
- }
643
- }
644
- };
645
- walk(null);
646
- return result;
647
- }
648
- function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], orderingStrategy = "integer", maxDepth) {
649
- const byId = cloneMap(state.byId);
650
- const byParent = cloneParentMap(state.byParent);
651
- const dragged = byId.get(String(activeId));
652
- if (!dragged) return state;
653
- const isRootStart = targetZone === "root-start";
654
- const isRootEnd = targetZone === "root-end";
655
- const isEnd = targetZone.startsWith("end-") || isRootEnd;
656
- const zoneTargetId = extractUUID(targetZone);
657
- const isAfter = targetZone.startsWith("after-");
658
- const isInto = targetZone.startsWith("into-") || isRootStart;
659
- const target = byId.get(zoneTargetId);
660
- const oldParentId = dragged.parentId ?? null;
661
- const newParentId = isRootStart || isRootEnd ? null : isInto || isEnd ? zoneTargetId : target?.parentId ?? null;
662
- if (containerTypes.includes(dragged.type) && newParentId !== null) {
663
- const newParent = byId.get(newParentId);
664
- if (newParent && !containerTypes.includes(newParent.type)) {
665
- return state;
666
- }
667
- }
668
- if (maxDepth != null) {
669
- const parentDepth = newParentId !== null ? getBlockDepth(state, newParentId) : 0;
670
- const subtreeDepth = getSubtreeDepth(state, dragged.id);
671
- if (parentDepth + subtreeDepth > maxDepth) return state;
672
- }
673
- if (dragged.id === zoneTargetId) return state;
674
- if (newParentId !== null && getDescendantIds(state, dragged.id).has(newParentId)) {
675
- return state;
676
- }
677
- const oldList = byParent.get(oldParentId) ?? [];
678
- const currentIndexInOldParent = oldList.indexOf(dragged.id);
679
- const preNewList = byParent.get(newParentId) ?? [];
680
- let targetIndex;
681
- if (isInto) {
682
- targetIndex = 0;
683
- } else if (isEnd) {
684
- targetIndex = preNewList.length;
685
- } else {
686
- const idx = preNewList.indexOf(zoneTargetId);
687
- targetIndex = idx === -1 ? preNewList.length : isAfter ? idx + 1 : idx;
688
- }
689
- if (oldParentId === newParentId && currentIndexInOldParent !== -1) {
690
- const adjustedTarget = targetIndex > currentIndexInOldParent ? targetIndex - 1 : targetIndex;
691
- if (adjustedTarget === currentIndexInOldParent) {
692
- return state;
693
- }
694
- }
695
- const filtered = oldList.filter((id) => id !== dragged.id);
696
- byParent.set(oldParentId, filtered);
697
- const newList = [...byParent.get(newParentId) ?? []];
698
- let insertIndex;
699
- if (isInto) {
700
- insertIndex = 0;
701
- } else if (isEnd) {
702
- insertIndex = newList.length;
703
- } else {
704
- const idx = newList.indexOf(zoneTargetId);
705
- insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
706
- }
707
- newList.splice(insertIndex, 0, dragged.id);
708
- byParent.set(newParentId, newList);
709
- let newOrder = dragged.order;
710
- if (orderingStrategy === "fractional") {
711
- const siblings = newList;
712
- const movedIdx = siblings.indexOf(dragged.id);
713
- const prevId = movedIdx > 0 ? siblings[movedIdx - 1] : null;
714
- const nextId = movedIdx < siblings.length - 1 ? siblings[movedIdx + 1] : null;
715
- const prevOrder = prevId ? String(byId.get(prevId).order) : null;
716
- const nextOrder = nextId ? String(byId.get(nextId).order) : null;
717
- newOrder = generateKeyBetween(prevOrder, nextOrder);
718
- }
719
- byId.set(dragged.id, {
720
- ...dragged,
721
- parentId: newParentId,
722
- order: newOrder
723
- });
724
- return { byId, byParent };
725
- }
726
- function getBlockDepth(index, blockId) {
727
- let depth = 0;
728
- let current = blockId;
729
- const visited = /* @__PURE__ */ new Set();
730
- while (current !== null) {
731
- if (visited.has(current)) break;
732
- visited.add(current);
733
- depth++;
734
- const block = index.byId.get(current);
735
- current = block?.parentId ?? null;
736
- }
737
- return depth;
738
- }
739
- function getSubtreeDepth(index, blockId, visited = /* @__PURE__ */ new Set()) {
740
- if (visited.has(blockId)) return 0;
741
- visited.add(blockId);
742
- const children = index.byParent.get(blockId) ?? [];
743
- if (children.length === 0) return 1;
744
- let max = 0;
745
- for (const childId of children) {
746
- max = Math.max(max, getSubtreeDepth(index, childId, visited));
747
- }
748
- return 1 + max;
749
- }
750
- function getDescendantIds(state, parentId) {
751
- const toDelete = /* @__PURE__ */ new Set();
752
- const stack = [parentId];
753
- while (stack.length > 0) {
754
- const current = stack.pop();
755
- toDelete.add(current);
756
- const children = state.byParent.get(current) ?? [];
757
- stack.push(...children);
758
- }
759
- return toDelete;
760
- }
761
- function reparentMultipleBlocks(state, blockIds, targetZone, containerTypes = [], orderingStrategy = "integer", maxDepth) {
762
- if (blockIds.length === 0) return state;
763
- if (blockIds.length === 1) {
764
- return reparentBlockIndex(state, blockIds[0], targetZone, containerTypes, orderingStrategy, maxDepth);
765
- }
766
- let result = reparentBlockIndex(state, blockIds[0], targetZone, containerTypes, orderingStrategy, maxDepth);
767
- if (result === state) return state;
768
- for (let i = 1; i < blockIds.length; i++) {
769
- result = reparentBlockIndex(result, blockIds[i], `after-${blockIds[i - 1]}`, containerTypes, orderingStrategy, maxDepth);
770
- }
771
- return result;
772
- }
773
- function validateBlockTree(index) {
774
- const issues = [];
775
- for (const [id] of index.byId) {
776
- const visited = /* @__PURE__ */ new Set();
777
- let current = id;
778
- while (current !== null) {
779
- if (visited.has(current)) {
780
- issues.push(`Cycle detected: block "${id}" has a circular parentId chain`);
781
- break;
782
- }
783
- visited.add(current);
784
- const block = index.byId.get(current);
785
- current = block?.parentId ?? null;
786
- }
787
- }
788
- for (const [id, block] of index.byId) {
789
- if (block.parentId !== null && !index.byId.has(block.parentId)) {
790
- issues.push(`Orphan: block "${id}" references non-existent parent "${block.parentId}"`);
791
- }
792
- }
793
- for (const [parentId, childIds] of index.byParent) {
794
- for (const childId of childIds) {
795
- if (!index.byId.has(childId)) {
796
- issues.push(`Stale ref: byParent key "${parentId}" lists non-existent block "${childId}"`);
797
- }
798
- }
799
- }
800
- if (issues.length > 0) {
801
- for (const issue of issues) {
802
- console.warn(`[dnd-block-tree] ${issue}`);
803
- }
804
- }
805
- return { valid: issues.length === 0, issues };
806
- }
807
- function deleteBlockAndDescendants(state, id) {
808
- const byId = cloneMap(state.byId);
809
- const byParent = cloneParentMap(state.byParent);
810
- const idsToDelete = getDescendantIds(state, id);
811
- for (const deleteId of idsToDelete) {
812
- byId.delete(deleteId);
813
- byParent.delete(deleteId);
814
- }
815
- for (const [parent, list] of byParent.entries()) {
816
- byParent.set(parent, list.filter((itemId) => !idsToDelete.has(itemId)));
817
- }
818
- return { byId, byParent };
819
- }
820
- function getBlockPosition(blocks, blockId) {
821
- const block = blocks.find((b) => b.id === blockId);
822
- if (!block) return { parentId: null, index: 0 };
823
- const siblings = blocks.filter((b) => b.parentId === block.parentId);
824
- const index = siblings.findIndex((b) => b.id === blockId);
825
- return { parentId: block.parentId, index };
826
- }
827
- function computeInitialExpanded(blocks, containerTypes, initialExpanded) {
828
- if (initialExpanded === "none") {
829
- const expandedMap2 = {};
830
- const containers2 = blocks.filter((b) => containerTypes.includes(b.type));
831
- for (const container of containers2) {
832
- expandedMap2[container.id] = false;
833
- }
834
- return expandedMap2;
835
- }
836
- const expandedMap = {};
837
- const containers = blocks.filter((b) => containerTypes.includes(b.type));
838
- if (initialExpanded === "all" || initialExpanded === void 0) {
839
- for (const container of containers) {
840
- expandedMap[container.id] = true;
841
- }
842
- } else if (Array.isArray(initialExpanded)) {
843
- for (const id of initialExpanded) {
844
- expandedMap[id] = true;
845
- }
846
- }
847
- return expandedMap;
848
- }
849
- function getVisibleBlockIds(blocksByParent, containerTypes, expandedMap, parentId = null) {
850
- const result = [];
851
- const children = blocksByParent.get(parentId) ?? [];
852
- for (const block of children) {
853
- result.push(block.id);
854
- if (containerTypes.includes(block.type) && expandedMap[block.id] !== false) {
855
- result.push(...getVisibleBlockIds(blocksByParent, containerTypes, expandedMap, block.id));
856
- }
857
- }
858
- return result;
859
- }
860
- function BlockTree({
861
- blocks,
862
- renderers,
863
- containerTypes = [],
864
- onChange,
865
- dragOverlay,
866
- activationDistance = 8,
867
- previewDebounce = 150,
868
- className = "flex flex-col gap-1",
869
- dropZoneClassName,
870
- dropZoneActiveClassName,
871
- indentClassName,
872
- showDropPreview = true,
873
- // Callbacks
874
- onDragStart,
875
- onDragMove,
876
- onDragEnd,
877
- onDragCancel,
878
- onBeforeMove,
879
- onBlockMove,
880
- onExpandChange,
881
- onHoverChange,
882
- // Customization
883
- canDrag,
884
- canDrop,
885
- collisionDetection,
886
- sensors: sensorConfig,
887
- animation,
888
- initialExpanded,
889
- orderingStrategy = "integer",
890
- maxDepth,
891
- keyboardNavigation = false,
892
- multiSelect = false,
893
- selectedIds: externalSelectedIds,
894
- onSelectionChange,
895
- virtualize
896
- }) {
897
- const sensors = useConfiguredSensors({
898
- activationDistance: sensorConfig?.activationDistance ?? activationDistance,
899
- activationDelay: sensorConfig?.activationDelay,
900
- tolerance: sensorConfig?.tolerance,
901
- longPressDelay: sensorConfig?.longPressDelay
902
- });
903
- const initialExpandedMap = react.useMemo(
904
- () => computeInitialExpanded(blocks, containerTypes, initialExpanded),
905
- // Only compute on mount
906
- // eslint-disable-next-line react-hooks/exhaustive-deps
907
- []
908
- );
909
- const stateRef = react.useRef({
910
- activeId: null,
911
- hoverZone: null,
912
- expandedMap: initialExpandedMap,
913
- virtualState: null,
914
- isDragging: false
915
- });
916
- const initialBlocksRef = react.useRef([]);
917
- const cachedReorderRef = react.useRef(null);
918
- const fromPositionRef = react.useRef(null);
919
- const draggedIdsRef = react.useRef([]);
920
- const snapshotRectsRef = react.useRef(null);
921
- const needsResnapshot = react.useRef(false);
922
- const stickyCollisionRef = react.useRef(createStickyCollision(20, snapshotRectsRef));
923
- const snapshotZoneRects = react.useCallback(() => {
924
- const root = rootRef.current;
925
- if (!root) return;
926
- const zones = root.querySelectorAll("[data-zone-id]");
927
- const map = /* @__PURE__ */ new Map();
928
- zones.forEach((el) => {
929
- const id = el.getAttribute("data-zone-id");
930
- if (id) map.set(id, el.getBoundingClientRect());
931
- });
932
- snapshotRectsRef.current = map;
933
- }, []);
934
- const [, forceRender] = react.useReducer((x) => x + 1, 0);
935
- const debouncedSetVirtual = react.useRef(
936
- debounce((newBlocks) => {
937
- if (newBlocks) {
938
- stateRef.current.virtualState = computeNormalizedIndex(newBlocks);
939
- } else {
940
- stateRef.current.virtualState = null;
941
- }
942
- needsResnapshot.current = true;
943
- forceRender();
944
- }, previewDebounce)
945
- ).current;
946
- const debouncedDragMove = react.useRef(
947
- debounce((event) => {
948
- onDragMove?.(event);
949
- }, 50)
950
- ).current;
951
- const originalIndex = react.useMemo(
952
- () => computeNormalizedIndex(blocks, orderingStrategy),
953
- [blocks, orderingStrategy]
954
- );
955
- const blocksByParent = react.useMemo(() => {
956
- const map = /* @__PURE__ */ new Map();
957
- for (const [parentId, ids] of originalIndex.byParent.entries()) {
958
- map.set(parentId, ids.map((id) => originalIndex.byId.get(id)).filter(Boolean));
959
- }
960
- return map;
961
- }, [originalIndex]);
962
- const focusedIdRef = react.useRef(null);
963
- const rootRef = react.useRef(null);
964
- const lastClickedIdRef = react.useRef(null);
965
- const [internalSelectedIds, setInternalSelectedIds] = react.useReducer(
966
- (_, next) => next,
967
- /* @__PURE__ */ new Set()
968
- );
969
- const selectedIds = externalSelectedIds ?? internalSelectedIds;
970
- const setSelectedIds = react.useCallback((ids) => {
971
- if (onSelectionChange) {
972
- onSelectionChange(ids);
973
- } else {
974
- setInternalSelectedIds(ids);
975
- }
976
- }, [onSelectionChange]);
977
- const visibleBlockIds = react.useMemo(
978
- () => getVisibleBlockIds(blocksByParent, containerTypes, stateRef.current.expandedMap),
979
- [blocksByParent, containerTypes, stateRef.current.expandedMap]
980
- );
981
- const focusBlock = react.useCallback((id) => {
982
- focusedIdRef.current = id;
983
- if (id && rootRef.current) {
984
- const el = rootRef.current.querySelector(`[data-block-id="${id}"]`);
985
- el?.focus();
986
- }
987
- forceRender();
988
- }, []);
989
- const toggleExpandRef = react.useRef(() => {
990
- });
991
- const handleKeyDown = react.useCallback((event) => {
992
- if (!keyboardNavigation) return;
993
- const currentId = focusedIdRef.current;
994
- const currentIndex = currentId ? visibleBlockIds.indexOf(currentId) : -1;
995
- switch (event.key) {
996
- case "ArrowDown": {
997
- event.preventDefault();
998
- const nextIndex = currentIndex < visibleBlockIds.length - 1 ? currentIndex + 1 : currentIndex;
999
- focusBlock(visibleBlockIds[nextIndex] ?? null);
1000
- break;
1001
- }
1002
- case "ArrowUp": {
1003
- event.preventDefault();
1004
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : 0;
1005
- focusBlock(visibleBlockIds[prevIndex] ?? null);
1006
- break;
1007
- }
1008
- case "ArrowRight": {
1009
- event.preventDefault();
1010
- if (currentId) {
1011
- const block = originalIndex.byId.get(currentId);
1012
- if (block && containerTypes.includes(block.type)) {
1013
- if (stateRef.current.expandedMap[currentId] === false) {
1014
- toggleExpandRef.current(currentId);
1015
- } else {
1016
- const children = blocksByParent.get(currentId) ?? [];
1017
- if (children.length > 0) focusBlock(children[0].id);
1018
- }
1019
- }
1020
- }
1021
- break;
1022
- }
1023
- case "ArrowLeft": {
1024
- event.preventDefault();
1025
- if (currentId) {
1026
- const block = originalIndex.byId.get(currentId);
1027
- if (block && containerTypes.includes(block.type) && stateRef.current.expandedMap[currentId] !== false) {
1028
- toggleExpandRef.current(currentId);
1029
- } else if (block?.parentId) {
1030
- focusBlock(block.parentId);
1031
- }
1032
- }
1033
- break;
1034
- }
1035
- case "Enter":
1036
- case " ": {
1037
- event.preventDefault();
1038
- if (currentId) {
1039
- const block = originalIndex.byId.get(currentId);
1040
- if (block && containerTypes.includes(block.type)) {
1041
- toggleExpandRef.current(currentId);
1042
- }
1043
- }
1044
- break;
1045
- }
1046
- case "Home": {
1047
- event.preventDefault();
1048
- if (visibleBlockIds.length > 0) focusBlock(visibleBlockIds[0]);
1049
- break;
1050
- }
1051
- case "End": {
1052
- event.preventDefault();
1053
- if (visibleBlockIds.length > 0) focusBlock(visibleBlockIds[visibleBlockIds.length - 1]);
1054
- break;
1055
- }
1056
- }
1057
- }, [keyboardNavigation, visibleBlockIds, focusBlock, originalIndex, containerTypes, blocksByParent]);
1058
- const handleBlockClick = react.useCallback((blockId, event) => {
1059
- if (!multiSelect) return;
1060
- if (event.metaKey || event.ctrlKey) {
1061
- const next = new Set(selectedIds);
1062
- if (next.has(blockId)) {
1063
- next.delete(blockId);
1064
- } else {
1065
- next.add(blockId);
1066
- }
1067
- setSelectedIds(next);
1068
- } else if (event.shiftKey && lastClickedIdRef.current) {
1069
- const startIdx = visibleBlockIds.indexOf(lastClickedIdRef.current);
1070
- const endIdx = visibleBlockIds.indexOf(blockId);
1071
- if (startIdx !== -1 && endIdx !== -1) {
1072
- const [from, to] = startIdx < endIdx ? [startIdx, endIdx] : [endIdx, startIdx];
1073
- const next = new Set(selectedIds);
1074
- for (let i = from; i <= to; i++) {
1075
- next.add(visibleBlockIds[i]);
1076
- }
1077
- setSelectedIds(next);
1078
- }
1079
- } else {
1080
- setSelectedIds(/* @__PURE__ */ new Set([blockId]));
1081
- }
1082
- lastClickedIdRef.current = blockId;
1083
- }, [multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1084
- react.useEffect(() => {
1085
- if (!needsResnapshot.current || !stateRef.current.isDragging) return;
1086
- needsResnapshot.current = false;
1087
- requestAnimationFrame(() => {
1088
- snapshotZoneRects();
1089
- });
1090
- });
1091
- react.useEffect(() => {
1092
- if (!keyboardNavigation || !focusedIdRef.current || !rootRef.current) return;
1093
- const el = rootRef.current.querySelector(`[data-block-id="${focusedIdRef.current}"]`);
1094
- el?.focus();
1095
- });
1096
- const previewPosition = react.useMemo(() => {
1097
- if (!showDropPreview || !stateRef.current.virtualState || !stateRef.current.activeId) {
1098
- return null;
1099
- }
1100
- const virtualIndex = stateRef.current.virtualState;
1101
- const activeId = stateRef.current.activeId;
1102
- const block = virtualIndex.byId.get(activeId);
1103
- if (!block) return null;
1104
- const parentId = block.parentId ?? null;
1105
- const siblings = virtualIndex.byParent.get(parentId) ?? [];
1106
- const index = siblings.indexOf(activeId);
1107
- return { parentId, index };
1108
- }, [showDropPreview, stateRef.current.virtualState, stateRef.current.activeId]);
1109
- const activeBlock = stateRef.current.activeId ? originalIndex.byId.get(stateRef.current.activeId) ?? null : null;
1110
- const draggedBlock = activeBlock;
1111
- const handleDragStart = react.useCallback((event) => {
1112
- const id = String(event.active.id);
1113
- const block = blocks.find((b) => b.id === id);
1114
- if (!block) return;
1115
- if (canDrag && !canDrag(block)) {
1116
- return;
1117
- }
1118
- const dragEvent = {
1119
- block,
1120
- blockId: id
1121
- };
1122
- const result = onDragStart?.(dragEvent);
1123
- if (result === false) {
1124
- return;
1125
- }
1126
- fromPositionRef.current = getBlockPosition(blocks, id);
1127
- stickyCollisionRef.current.reset();
1128
- if (multiSelect && selectedIds.has(id)) {
1129
- draggedIdsRef.current = visibleBlockIds.filter((vid) => selectedIds.has(vid));
1130
- } else {
1131
- draggedIdsRef.current = [id];
1132
- if (multiSelect) {
1133
- setSelectedIds(/* @__PURE__ */ new Set([id]));
1134
- }
1135
- }
1136
- if (sensorConfig?.hapticFeedback) {
1137
- triggerHaptic();
1138
- }
1139
- stateRef.current.activeId = id;
1140
- stateRef.current.isDragging = true;
1141
- initialBlocksRef.current = [...blocks];
1142
- cachedReorderRef.current = null;
1143
- needsResnapshot.current = true;
1144
- forceRender();
1145
- }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
1146
- const handleDragMove = react.useCallback((event) => {
1147
- if (!onDragMove) return;
1148
- const id = stateRef.current.activeId;
1149
- if (!id) return;
1150
- const block = blocks.find((b) => b.id === id);
1151
- if (!block) return;
1152
- const moveEvent = {
1153
- block,
1154
- blockId: id,
1155
- overZone: stateRef.current.hoverZone,
1156
- coordinates: {
1157
- x: event.delta.x,
1158
- y: event.delta.y
1159
- }
1160
- };
1161
- debouncedDragMove(moveEvent);
1162
- }, [blocks, onDragMove, debouncedDragMove]);
1163
- const handleDragOver = react.useCallback((event) => {
1164
- if (!event.over) return;
1165
- const targetZone = String(event.over.id);
1166
- const activeId = stateRef.current.activeId;
1167
- if (!activeId) return;
1168
- const activeBlock2 = blocks.find((b) => b.id === activeId);
1169
- const targetBlockId = extractBlockId(targetZone);
1170
- const targetBlock = blocks.find((b) => b.id === targetBlockId) ?? null;
1171
- if (canDrop && activeBlock2 && !canDrop(activeBlock2, targetZone, targetBlock)) {
1172
- return;
1173
- }
1174
- if (maxDepth != null && activeBlock2) {
1175
- const baseIndex2 = computeNormalizedIndex(initialBlocksRef.current, orderingStrategy);
1176
- const testResult = reparentBlockIndex(baseIndex2, activeId, targetZone, containerTypes, orderingStrategy, maxDepth);
1177
- if (testResult === baseIndex2) return;
1178
- }
1179
- if (stateRef.current.hoverZone !== targetZone) {
1180
- const zoneType = getDropZoneType(targetZone);
1181
- const hoverEvent = {
1182
- zoneId: targetZone,
1183
- zoneType,
1184
- targetBlock
1185
- };
1186
- onHoverChange?.(hoverEvent);
1187
- }
1188
- stateRef.current.hoverZone = targetZone;
1189
- const baseIndex = computeNormalizedIndex(initialBlocksRef.current, orderingStrategy);
1190
- const ids = draggedIdsRef.current;
1191
- const updatedIndex = ids.length > 1 ? reparentMultipleBlocks(baseIndex, ids, targetZone, containerTypes, orderingStrategy, maxDepth) : reparentBlockIndex(baseIndex, activeId, targetZone, containerTypes, orderingStrategy, maxDepth);
1192
- const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes, orderingStrategy);
1193
- cachedReorderRef.current = { targetId: targetZone, reorderedBlocks: orderedBlocks };
1194
- if (showDropPreview) {
1195
- debouncedSetVirtual(orderedBlocks);
1196
- }
1197
- }, [blocks, containerTypes, debouncedSetVirtual, canDrop, onHoverChange, showDropPreview, maxDepth, orderingStrategy]);
1198
- const handleDragEnd = react.useCallback((_event) => {
1199
- debouncedSetVirtual.cancel();
1200
- debouncedDragMove.cancel();
1201
- let cached = cachedReorderRef.current;
1202
- const activeId = stateRef.current.activeId;
1203
- const activeBlockData = activeId ? blocks.find((b) => b.id === activeId) : null;
1204
- if (cached && activeBlockData && fromPositionRef.current && onBeforeMove) {
1205
- const operation = {
1206
- block: activeBlockData,
1207
- from: fromPositionRef.current,
1208
- targetZone: cached.targetId
1209
- };
1210
- const result = onBeforeMove(operation);
1211
- if (result === false) {
1212
- stateRef.current.activeId = null;
1213
- stateRef.current.hoverZone = null;
1214
- stateRef.current.virtualState = null;
1215
- stateRef.current.isDragging = false;
1216
- cachedReorderRef.current = null;
1217
- initialBlocksRef.current = [];
1218
- fromPositionRef.current = null;
1219
- snapshotRectsRef.current = null;
1220
- forceRender();
1221
- return;
1222
- }
1223
- if (result && result.targetZone !== cached.targetId) {
1224
- const baseIndex = computeNormalizedIndex(initialBlocksRef.current, orderingStrategy);
1225
- const ids = draggedIdsRef.current;
1226
- const updatedIndex = ids.length > 1 ? reparentMultipleBlocks(baseIndex, ids, result.targetZone, containerTypes, orderingStrategy, maxDepth) : reparentBlockIndex(baseIndex, activeId, result.targetZone, containerTypes, orderingStrategy, maxDepth);
1227
- const reorderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes, orderingStrategy);
1228
- cached = { targetId: result.targetZone, reorderedBlocks };
1229
- }
1230
- }
1231
- if (activeBlockData) {
1232
- const endEvent = {
1233
- block: activeBlockData,
1234
- blockId: activeId,
1235
- targetZone: cached?.targetId ?? null,
1236
- cancelled: false
1237
- };
1238
- onDragEnd?.(endEvent);
1239
- }
1240
- if (cached && activeBlockData && fromPositionRef.current) {
1241
- const toPosition = getBlockPosition(cached.reorderedBlocks, activeBlockData.id);
1242
- const moveEvent = {
1243
- block: activeBlockData,
1244
- from: fromPositionRef.current,
1245
- to: toPosition,
1246
- blocks: cached.reorderedBlocks,
1247
- movedIds: [...draggedIdsRef.current]
1248
- };
1249
- onBlockMove?.(moveEvent);
1250
- }
1251
- stateRef.current.activeId = null;
1252
- stateRef.current.hoverZone = null;
1253
- stateRef.current.virtualState = null;
1254
- stateRef.current.isDragging = false;
1255
- cachedReorderRef.current = null;
1256
- initialBlocksRef.current = [];
1257
- fromPositionRef.current = null;
1258
- draggedIdsRef.current = [];
1259
- snapshotRectsRef.current = null;
1260
- if (cached && onChange) {
1261
- onChange(cached.reorderedBlocks);
1262
- }
1263
- forceRender();
1264
- }, [blocks, containerTypes, orderingStrategy, debouncedSetVirtual, debouncedDragMove, onChange, onDragEnd, onBlockMove, onBeforeMove]);
1265
- const handleDragCancel = react.useCallback((_event) => {
1266
- debouncedSetVirtual.cancel();
1267
- debouncedDragMove.cancel();
1268
- const activeId = stateRef.current.activeId;
1269
- const activeBlockData = activeId ? blocks.find((b) => b.id === activeId) : null;
1270
- if (activeBlockData) {
1271
- const cancelEvent = {
1272
- block: activeBlockData,
1273
- blockId: activeId,
1274
- targetZone: null,
1275
- cancelled: true
1276
- };
1277
- onDragCancel?.(cancelEvent);
1278
- onDragEnd?.(cancelEvent);
1279
- }
1280
- stateRef.current.activeId = null;
1281
- stateRef.current.hoverZone = null;
1282
- stateRef.current.virtualState = null;
1283
- stateRef.current.isDragging = false;
1284
- cachedReorderRef.current = null;
1285
- initialBlocksRef.current = [];
1286
- fromPositionRef.current = null;
1287
- draggedIdsRef.current = [];
1288
- snapshotRectsRef.current = null;
1289
- forceRender();
1290
- }, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
1291
- const handleHover = react.useCallback((zoneId, _parentId) => {
1292
- const activeId = stateRef.current.activeId;
1293
- if (!activeId) return;
1294
- const activeBlockData = blocks.find((b) => b.id === activeId);
1295
- const targetBlockId = extractBlockId(zoneId);
1296
- const targetBlock = blocks.find((b) => b.id === targetBlockId) ?? null;
1297
- if (canDrop && activeBlockData && !canDrop(activeBlockData, zoneId, targetBlock)) {
1298
- return;
1299
- }
1300
- if (maxDepth != null && activeBlockData) {
1301
- const baseIdx = computeNormalizedIndex(initialBlocksRef.current, orderingStrategy);
1302
- const testResult = reparentBlockIndex(baseIdx, activeId, zoneId, containerTypes, orderingStrategy, maxDepth);
1303
- if (testResult === baseIdx) return;
1304
- }
1305
- if (stateRef.current.hoverZone !== zoneId) {
1306
- const zoneType = getDropZoneType(zoneId);
1307
- const hoverEvent = {
1308
- zoneId,
1309
- zoneType,
1310
- targetBlock
1311
- };
1312
- onHoverChange?.(hoverEvent);
1313
- }
1314
- stateRef.current.hoverZone = zoneId;
1315
- const baseIndex = computeNormalizedIndex(initialBlocksRef.current, orderingStrategy);
1316
- const ids = draggedIdsRef.current;
1317
- const updatedIndex = ids.length > 1 ? reparentMultipleBlocks(baseIndex, ids, zoneId, containerTypes, orderingStrategy, maxDepth) : reparentBlockIndex(baseIndex, activeId, zoneId, containerTypes, orderingStrategy, maxDepth);
1318
- const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes, orderingStrategy);
1319
- cachedReorderRef.current = { targetId: zoneId, reorderedBlocks: orderedBlocks };
1320
- if (showDropPreview) {
1321
- debouncedSetVirtual(orderedBlocks);
1322
- }
1323
- }, [blocks, containerTypes, orderingStrategy, debouncedSetVirtual, canDrop, onHoverChange, showDropPreview, maxDepth]);
1324
- const handleToggleExpand = react.useCallback((id) => {
1325
- const newExpanded = stateRef.current.expandedMap[id] === false;
1326
- stateRef.current.expandedMap = {
1327
- ...stateRef.current.expandedMap,
1328
- [id]: newExpanded
1329
- };
1330
- const block = blocks.find((b) => b.id === id);
1331
- if (block && onExpandChange) {
1332
- const expandEvent = {
1333
- block,
1334
- blockId: id,
1335
- expanded: newExpanded
1336
- };
1337
- onExpandChange(expandEvent);
1338
- }
1339
- forceRender();
1340
- }, [blocks, onExpandChange]);
1341
- toggleExpandRef.current = handleToggleExpand;
1342
- const virtualContainerRef = react.useRef(null);
1343
- const [virtualScroll, setVirtualScroll] = react.useState({ scrollTop: 0, clientHeight: 0 });
1344
- react.useEffect(() => {
1345
- if (!virtualize) return;
1346
- const el = virtualContainerRef.current;
1347
- if (!el) return;
1348
- setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1349
- const onScroll = () => {
1350
- setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1351
- };
1352
- el.addEventListener("scroll", onScroll, { passive: true });
1353
- return () => el.removeEventListener("scroll", onScroll);
1354
- }, [virtualize]);
1355
- const virtualResult = react.useMemo(() => {
1356
- if (!virtualize) return null;
1357
- const { itemHeight, overscan = 5 } = virtualize;
1358
- const { scrollTop, clientHeight } = virtualScroll;
1359
- const totalHeight = visibleBlockIds.length * itemHeight;
1360
- const startRaw = Math.floor(scrollTop / itemHeight);
1361
- const visibleCount = Math.ceil(clientHeight / itemHeight);
1362
- const start = Math.max(0, startRaw - overscan);
1363
- const end = Math.min(visibleBlockIds.length - 1, startRaw + visibleCount + overscan);
1364
- const offsetY = start * itemHeight;
1365
- const visibleSet = /* @__PURE__ */ new Set();
1366
- for (let i = start; i <= end; i++) {
1367
- visibleSet.add(visibleBlockIds[i]);
1368
- }
1369
- return { totalHeight, offsetY, visibleSet };
1370
- }, [virtualize, virtualScroll, visibleBlockIds]);
1371
- const treeContent = /* @__PURE__ */ jsxRuntime.jsx(
1372
- TreeRenderer,
1373
- {
1374
- blocks,
1375
- blocksByParent,
1376
- parentId: null,
1377
- activeId: stateRef.current.activeId,
1378
- expandedMap: stateRef.current.expandedMap,
1379
- renderers,
1380
- containerTypes,
1381
- onHover: handleHover,
1382
- onToggleExpand: handleToggleExpand,
1383
- dropZoneClassName,
1384
- dropZoneActiveClassName,
1385
- indentClassName,
1386
- rootClassName: className,
1387
- canDrag,
1388
- previewPosition,
1389
- draggedBlock,
1390
- focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1391
- selectedIds: multiSelect ? selectedIds : void 0,
1392
- onBlockClick: multiSelect ? handleBlockClick : void 0,
1393
- animation,
1394
- virtualVisibleIds: virtualResult?.visibleSet ?? null
1395
- }
1396
- );
1397
- return /* @__PURE__ */ jsxRuntime.jsxs(
1398
- core.DndContext,
1399
- {
1400
- sensors,
1401
- collisionDetection: collisionDetection ?? stickyCollisionRef.current,
1402
- onDragStart: handleDragStart,
1403
- onDragMove: handleDragMove,
1404
- onDragOver: handleDragOver,
1405
- onDragEnd: handleDragEnd,
1406
- onDragCancel: handleDragCancel,
1407
- children: [
1408
- virtualize ? /* @__PURE__ */ jsxRuntime.jsx(
1409
- "div",
1410
- {
1411
- ref: (el) => {
1412
- virtualContainerRef.current = el;
1413
- rootRef.current = el;
1414
- },
1415
- className,
1416
- style: { minWidth: 0, overflow: "auto", position: "relative" },
1417
- onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1418
- role: keyboardNavigation ? "tree" : void 0,
1419
- 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 }) })
1420
- }
1421
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1422
- "div",
1423
- {
1424
- ref: rootRef,
1425
- className,
1426
- style: { minWidth: 0 },
1427
- onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1428
- role: keyboardNavigation ? "tree" : void 0,
1429
- children: treeContent
1430
- }
1431
- ),
1432
- /* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { activeBlock, selectedCount: multiSelect ? selectedIds.size : 0, children: dragOverlay })
1433
- ]
1434
- }
1435
- );
1436
- }
1437
- function BlockTreeSSR({ fallback = null, ...props }) {
1438
- const [mounted, setMounted] = react.useState(false);
1439
- react.useEffect(() => {
1440
- setMounted(true);
1441
- }, []);
1442
- if (!mounted) {
1443
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
1444
- }
1445
- return /* @__PURE__ */ jsxRuntime.jsx(BlockTree, { ...props });
1446
- }
1447
- function blockReducer(state, action, containerTypes = [], orderingStrategy = "integer", maxDepth) {
1448
- switch (action.type) {
1449
- case "ADD_ITEM": {
1450
- const byId = cloneMap(state.byId);
1451
- const byParent = cloneParentMap(state.byParent);
1452
- const item = action.payload;
1453
- byId.set(item.id, item);
1454
- const parentKey = item.parentId ?? null;
1455
- const list = byParent.get(parentKey) ?? [];
1456
- const insertAt = typeof item.order === "number" && item.order <= list.length ? item.order : list.length;
1457
- const newList = [...list];
1458
- newList.splice(insertAt, 0, item.id);
1459
- byParent.set(parentKey, newList);
1460
- return { byId, byParent };
1461
- }
1462
- case "INSERT_ITEM": {
1463
- const { item, parentId, index } = action.payload;
1464
- const updated = new Map(state.byParent);
1465
- const siblings = [...updated.get(parentId) ?? []];
1466
- siblings.splice(index, 0, item.id);
1467
- updated.set(parentId, siblings);
1468
- return {
1469
- byId: new Map(state.byId).set(item.id, item),
1470
- byParent: updated
1471
- };
1472
- }
1473
- case "DELETE_ITEM": {
1474
- return deleteBlockAndDescendants(state, action.payload.id);
1475
- }
1476
- case "SET_ALL": {
1477
- return computeNormalizedIndex(action.payload);
1478
- }
1479
- case "MOVE_ITEM": {
1480
- return reparentBlockIndex(
1481
- state,
1482
- action.payload.activeId,
1483
- action.payload.targetZone,
1484
- containerTypes,
1485
- orderingStrategy,
1486
- maxDepth
1487
- );
1488
- }
1489
- default:
1490
- return state;
1491
- }
1492
- }
1493
- function createBlockState() {
1494
- const BlockContext = react.createContext(null);
1495
- function useBlockState() {
1496
- const ctx = react.useContext(BlockContext);
1497
- if (!ctx) throw new Error("useBlockState must be used inside BlockStateProvider");
1498
- return ctx;
1499
- }
1500
- function BlockStateProvider({
1501
- children,
1502
- initialBlocks = [],
1503
- containerTypes = [],
1504
- onChange,
1505
- orderingStrategy = "integer",
1506
- maxDepth,
1507
- onBlockAdd,
1508
- onBlockDelete
1509
- }) {
1510
- const reducerWithOptions = react.useCallback(
1511
- (state2, action) => blockReducer(state2, action, containerTypes, orderingStrategy, maxDepth),
1512
- [containerTypes, orderingStrategy, maxDepth]
1513
- );
1514
- const [state, dispatch] = react.useReducer(
1515
- reducerWithOptions,
1516
- computeNormalizedIndex(initialBlocks, orderingStrategy)
1517
- );
1518
- const blocks = react.useMemo(() => {
1519
- const result = [];
1520
- const walk = (parentId) => {
1521
- const children2 = state.byParent.get(parentId) ?? [];
1522
- for (let i = 0; i < children2.length; i++) {
1523
- const id = children2[i];
1524
- const b = state.byId.get(id);
1525
- if (b) {
1526
- result.push(orderingStrategy === "fractional" ? b : { ...b, order: i });
1527
- if (containerTypes.includes(b.type)) walk(b.id);
1528
- }
1529
- }
1530
- };
1531
- walk(null);
1532
- return result;
1533
- }, [state, containerTypes, orderingStrategy]);
1534
- react.useMemo(() => {
1535
- onChange?.(blocks);
1536
- }, [blocks, onChange]);
1537
- const blockMap = react.useMemo(() => state.byId, [state]);
1538
- const childrenMap = react.useMemo(() => {
1539
- const map = /* @__PURE__ */ new Map();
1540
- for (const [parentId, ids] of state.byParent.entries()) {
1541
- map.set(
1542
- parentId,
1543
- ids.map((id) => state.byId.get(id)).filter(Boolean)
1544
- );
1545
- }
1546
- return map;
1547
- }, [state]);
1548
- const indexMap = react.useMemo(() => {
1549
- const map = /* @__PURE__ */ new Map();
1550
- for (const ids of state.byParent.values()) {
1551
- ids.forEach((id, index) => {
1552
- map.set(id, index);
1553
- });
1554
- }
1555
- return map;
1556
- }, [state]);
1557
- const createItem = react.useCallback(
1558
- (type, parentId = null) => {
1559
- const siblings = state.byParent.get(parentId) ?? [];
1560
- let order = siblings.length;
1561
- if (orderingStrategy === "fractional") {
1562
- const lastId = siblings[siblings.length - 1];
1563
- const lastOrder = lastId ? String(state.byId.get(lastId).order) : null;
1564
- order = generateKeyBetween(lastOrder, null);
1565
- }
1566
- const newItem = { id: generateId(), type, parentId, order };
1567
- dispatch({ type: "ADD_ITEM", payload: newItem });
1568
- onBlockAdd?.({ block: newItem, parentId, index: siblings.length });
1569
- return newItem;
1570
- },
1571
- [state, orderingStrategy, onBlockAdd]
1572
- );
1573
- const insertItem = react.useCallback(
1574
- (type, referenceId, position) => {
1575
- const referenceBlock = state.byId.get(referenceId);
1576
- if (!referenceBlock) throw new Error(`Reference block ${referenceId} not found`);
1577
- const parentId = referenceBlock.parentId ?? null;
1578
- const siblings = state.byParent.get(parentId) ?? [];
1579
- const index = siblings.indexOf(referenceId);
1580
- const insertIndex = position === "before" ? index : index + 1;
1581
- let order = insertIndex;
1582
- if (orderingStrategy === "fractional") {
1583
- const prevId = insertIndex > 0 ? siblings[insertIndex - 1] : null;
1584
- const nextId = insertIndex < siblings.length ? siblings[insertIndex] : null;
1585
- const prevOrder = prevId ? String(state.byId.get(prevId).order) : null;
1586
- const nextOrder = nextId ? String(state.byId.get(nextId).order) : null;
1587
- order = generateKeyBetween(prevOrder, nextOrder);
1588
- }
1589
- const newItem = { id: generateId(), type, parentId, order };
1590
- dispatch({
1591
- type: "INSERT_ITEM",
1592
- payload: { item: newItem, parentId, index: insertIndex }
1593
- });
1594
- onBlockAdd?.({ block: newItem, parentId, index: insertIndex });
1595
- return newItem;
1596
- },
1597
- [state, orderingStrategy, onBlockAdd]
1598
- );
1599
- const deleteItem = react.useCallback((id) => {
1600
- const block = state.byId.get(id);
1601
- if (block && onBlockDelete) {
1602
- const deletedIds = [...getDescendantIds(state, id)];
1603
- onBlockDelete({ block, deletedIds, parentId: block.parentId });
1604
- }
1605
- dispatch({ type: "DELETE_ITEM", payload: { id } });
1606
- }, [state, onBlockDelete]);
1607
- const moveItem = react.useCallback((activeId, targetZone) => {
1608
- dispatch({ type: "MOVE_ITEM", payload: { activeId, targetZone } });
1609
- }, []);
1610
- const setAll = react.useCallback((all) => {
1611
- dispatch({ type: "SET_ALL", payload: all });
1612
- }, []);
1613
- const value = react.useMemo(
1614
- () => ({
1615
- blocks,
1616
- blockMap,
1617
- childrenMap,
1618
- indexMap,
1619
- normalizedIndex: state,
1620
- createItem,
1621
- insertItem,
1622
- deleteItem,
1623
- moveItem,
1624
- setAll
1625
- }),
1626
- [
1627
- blocks,
1628
- blockMap,
1629
- childrenMap,
1630
- indexMap,
1631
- state,
1632
- createItem,
1633
- insertItem,
1634
- deleteItem,
1635
- moveItem,
1636
- setAll
1637
- ]
1638
- );
1639
- return /* @__PURE__ */ jsxRuntime.jsx(BlockContext.Provider, { value, children });
1640
- }
1641
- return {
1642
- BlockStateProvider,
1643
- useBlockState
1644
- };
1645
- }
1646
- function expandReducer(state, action) {
1647
- switch (action.type) {
1648
- case "TOGGLE":
1649
- return { ...state, [action.id]: !state[action.id] };
1650
- case "SET_ALL": {
1651
- const newState = {};
1652
- for (const id of action.ids) {
1653
- newState[id] = action.expanded;
1654
- }
1655
- return newState;
1656
- }
1657
- default:
1658
- return state;
1659
- }
1660
- }
1661
- function createTreeState(options = {}) {
1662
- const { previewDebounce = 150, containerTypes = [] } = options;
1663
- const TreeContext = react.createContext(null);
1664
- function useTreeState() {
1665
- const ctx = react.useContext(TreeContext);
1666
- if (!ctx) throw new Error("useTreeState must be used inside TreeStateProvider");
1667
- return ctx;
1668
- }
1669
- function TreeStateProvider({ children, blocks, blockMap }) {
1670
- const [activeId, setActiveId] = react.useState(null);
1671
- const [hoverZone, setHoverZone] = react.useState(null);
1672
- const [virtualState, setVirtualState] = react.useState(null);
1673
- const [expandedMap, dispatchExpand] = react.useReducer(expandReducer, {});
1674
- const initialBlocksRef = react.useRef([]);
1675
- const cachedReorderRef = react.useRef(null);
1676
- const activeBlock = react.useMemo(() => {
1677
- if (!activeId) return null;
1678
- return blockMap.get(activeId) ?? null;
1679
- }, [activeId, blockMap]);
1680
- const debouncedSetVirtualBlocks = react.useMemo(
1681
- () => debounce((newBlocks) => {
1682
- if (!newBlocks) {
1683
- setVirtualState(null);
1684
- } else {
1685
- setVirtualState(computeNormalizedIndex(newBlocks));
1686
- }
1687
- }, previewDebounce),
1688
- [previewDebounce]
1689
- );
1690
- const effectiveState = react.useMemo(() => {
1691
- return virtualState ?? computeNormalizedIndex(blocks);
1692
- }, [virtualState, blocks]);
1693
- const effectiveBlocks = react.useMemo(() => {
1694
- return buildOrderedBlocks(effectiveState, containerTypes);
1695
- }, [effectiveState, containerTypes]);
1696
- const blocksByParent = react.useMemo(() => {
1697
- const map = /* @__PURE__ */ new Map();
1698
- for (const [parentId, ids] of effectiveState.byParent.entries()) {
1699
- map.set(
1700
- parentId,
1701
- ids.map((id) => effectiveState.byId.get(id)).filter(Boolean)
1702
- );
1703
- }
1704
- return map;
1705
- }, [effectiveState]);
1706
- const handleDragStart = react.useCallback(
1707
- (id) => {
1708
- setActiveId(id);
1709
- if (id) {
1710
- initialBlocksRef.current = [...blocks];
1711
- cachedReorderRef.current = null;
1712
- }
1713
- },
1714
- [blocks]
1715
- );
1716
- const handleDragOver = react.useCallback(
1717
- (targetZone) => {
1718
- if (!activeId) return;
1719
- setHoverZone(targetZone);
1720
- const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
1721
- const updatedIndex = reparentBlockIndex(baseIndex, activeId, targetZone, containerTypes);
1722
- const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
1723
- cachedReorderRef.current = { targetId: targetZone, reorderedBlocks: orderedBlocks };
1724
- debouncedSetVirtualBlocks(orderedBlocks);
1725
- },
1726
- [activeId, debouncedSetVirtualBlocks, containerTypes]
1727
- );
1728
- react.useCallback(() => {
1729
- debouncedSetVirtualBlocks.cancel();
1730
- setVirtualState(null);
1731
- setActiveId(null);
1732
- setHoverZone(null);
1733
- const result = cachedReorderRef.current;
1734
- cachedReorderRef.current = null;
1735
- initialBlocksRef.current = [];
1736
- return result;
1737
- }, [debouncedSetVirtualBlocks]);
1738
- const handleHover = react.useCallback(
1739
- (zoneId, _parentId) => {
1740
- if (!activeId) return;
1741
- handleDragOver(zoneId);
1742
- },
1743
- [activeId, handleDragOver]
1744
- );
1745
- const toggleExpand = react.useCallback((id) => {
1746
- dispatchExpand({ type: "TOGGLE", id });
1747
- }, []);
1748
- const setExpandAll = react.useCallback(
1749
- (expanded) => {
1750
- const containerIds = blocks.filter((b) => containerTypes.includes(b.type)).map((b) => b.id);
1751
- dispatchExpand({ type: "SET_ALL", expanded, ids: containerIds });
1752
- },
1753
- [blocks, containerTypes]
1754
- );
1755
- react.useEffect(() => {
1756
- return () => {
1757
- debouncedSetVirtualBlocks.cancel();
1758
- };
1759
- }, [debouncedSetVirtualBlocks]);
1760
- const value = react.useMemo(
1761
- () => ({
1762
- activeId,
1763
- activeBlock,
1764
- hoverZone,
1765
- expandedMap,
1766
- effectiveBlocks,
1767
- blocksByParent,
1768
- setActiveId: handleDragStart,
1769
- setHoverZone,
1770
- toggleExpand,
1771
- setExpandAll,
1772
- handleHover
1773
- }),
1774
- [
1775
- activeId,
1776
- activeBlock,
1777
- hoverZone,
1778
- expandedMap,
1779
- effectiveBlocks,
1780
- blocksByParent,
1781
- handleDragStart,
1782
- toggleExpand,
1783
- setExpandAll,
1784
- handleHover
1785
- ]
1786
- );
1787
- return /* @__PURE__ */ jsxRuntime.jsx(TreeContext.Provider, { value, children });
1788
- }
1789
- return {
1790
- TreeStateProvider,
1791
- useTreeState
1792
- };
1793
- }
1794
- function historyReducer(state, action) {
1795
- switch (action.type) {
1796
- case "SET": {
1797
- const past = [...state.past, state.present];
1798
- if (past.length > action.maxSteps) {
1799
- past.shift();
1800
- }
1801
- return {
1802
- past,
1803
- present: action.payload,
1804
- future: []
1805
- };
1806
- }
1807
- case "UNDO": {
1808
- if (state.past.length === 0) return state;
1809
- const previous = state.past[state.past.length - 1];
1810
- return {
1811
- past: state.past.slice(0, -1),
1812
- present: previous,
1813
- future: [state.present, ...state.future]
1814
- };
1815
- }
1816
- case "REDO": {
1817
- if (state.future.length === 0) return state;
1818
- const next = state.future[0];
1819
- return {
1820
- past: [...state.past, state.present],
1821
- present: next,
1822
- future: state.future.slice(1)
1823
- };
1824
- }
1825
- default:
1826
- return state;
1827
- }
1828
- }
1829
- function useBlockHistory(initialBlocks, options = {}) {
1830
- const { maxSteps = 50 } = options;
1831
- const [state, dispatch] = react.useReducer(historyReducer, {
1832
- past: [],
1833
- present: initialBlocks,
1834
- future: []
1835
- });
1836
- const set = react.useCallback(
1837
- (blocks) => dispatch({ type: "SET", payload: blocks, maxSteps }),
1838
- [maxSteps]
1839
- );
1840
- const undo = react.useCallback(() => dispatch({ type: "UNDO" }), []);
1841
- const redo = react.useCallback(() => dispatch({ type: "REDO" }), []);
1842
- return {
1843
- blocks: state.present,
1844
- set,
1845
- undo,
1846
- redo,
1847
- canUndo: state.past.length > 0,
1848
- canRedo: state.future.length > 0
1849
- };
1850
- }
1851
- function useLayoutAnimation(containerRef, options = {}) {
1852
- const {
1853
- duration = 200,
1854
- easing = "ease",
1855
- selector = "[data-block-id]"
1856
- } = options;
1857
- const prevPositions = react.useRef(/* @__PURE__ */ new Map());
1858
- react.useLayoutEffect(() => {
1859
- const container = containerRef.current;
1860
- if (!container) return;
1861
- const children = container.querySelectorAll(selector);
1862
- const currentPositions = /* @__PURE__ */ new Map();
1863
- children.forEach((el) => {
1864
- const id = el.dataset.blockId;
1865
- if (id) {
1866
- currentPositions.set(id, el.getBoundingClientRect());
1867
- }
1868
- });
1869
- children.forEach((el) => {
1870
- const htmlEl = el;
1871
- const id = htmlEl.dataset.blockId;
1872
- if (!id) return;
1873
- const prev = prevPositions.current.get(id);
1874
- const curr = currentPositions.get(id);
1875
- if (!prev || !curr) return;
1876
- const deltaY = prev.top - curr.top;
1877
- const deltaX = prev.left - curr.left;
1878
- if (deltaY === 0 && deltaX === 0) return;
1879
- htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
1880
- htmlEl.style.transition = "none";
1881
- requestAnimationFrame(() => {
1882
- htmlEl.style.transition = `transform ${duration}ms ${easing}`;
1883
- htmlEl.style.transform = "";
1884
- const onEnd = () => {
1885
- htmlEl.style.transition = "";
1886
- htmlEl.removeEventListener("transitionend", onEnd);
1887
- };
1888
- htmlEl.addEventListener("transitionend", onEnd);
1889
- });
1890
- });
1891
- prevPositions.current = currentPositions;
1892
- });
1893
- }
1894
- function useVirtualTree({
1895
- containerRef,
1896
- itemCount,
1897
- itemHeight,
1898
- overscan = 5
1899
- }) {
1900
- const [scrollTop, setScrollTop] = react.useState(0);
1901
- const [containerHeight, setContainerHeight] = react.useState(0);
1902
- const rafId = react.useRef(0);
1903
- const handleScroll = react.useCallback(() => {
1904
- cancelAnimationFrame(rafId.current);
1905
- rafId.current = requestAnimationFrame(() => {
1906
- const el = containerRef.current;
1907
- if (el) {
1908
- setScrollTop(el.scrollTop);
1909
- setContainerHeight(el.clientHeight);
1910
- }
1911
- });
1912
- }, [containerRef]);
1913
- react.useEffect(() => {
1914
- const el = containerRef.current;
1915
- if (!el) return;
1916
- setScrollTop(el.scrollTop);
1917
- setContainerHeight(el.clientHeight);
1918
- el.addEventListener("scroll", handleScroll, { passive: true });
1919
- return () => {
1920
- el.removeEventListener("scroll", handleScroll);
1921
- cancelAnimationFrame(rafId.current);
1922
- };
1923
- }, [containerRef, handleScroll]);
1924
- const totalHeight = itemCount * itemHeight;
1925
- const startRaw = Math.floor(scrollTop / itemHeight);
1926
- const visibleCount = Math.ceil(containerHeight / itemHeight);
1927
- const start = Math.max(0, startRaw - overscan);
1928
- const end = Math.min(itemCount - 1, startRaw + visibleCount + overscan);
1929
- const offsetY = start * itemHeight;
1930
- return {
1931
- visibleRange: { start, end },
1932
- totalHeight,
1933
- offsetY
1934
- };
1935
- }
1936
- var MAX_EVENTS = 100;
1937
- function useDevToolsCallbacks() {
1938
- const [events, setEvents] = react.useState([]);
1939
- const nextIdRef = react.useRef(1);
1940
- const addEvent = react.useCallback((type, summary) => {
1941
- const entry = {
1942
- id: nextIdRef.current++,
1943
- timestamp: Date.now(),
1944
- type,
1945
- summary
1946
- };
1947
- setEvents((prev) => [entry, ...prev].slice(0, MAX_EVENTS));
1948
- }, []);
1949
- const clearEvents = react.useCallback(() => {
1950
- setEvents([]);
1951
- }, []);
1952
- const callbacks = react.useMemo(() => ({
1953
- onDragStart: (event) => {
1954
- addEvent("dragStart", `Started dragging "${event.blockId}"`);
1955
- },
1956
- onDragEnd: (event) => {
1957
- if (event.cancelled) {
1958
- addEvent("dragEnd", `Cancelled drag of "${event.blockId}"`);
1959
- } else {
1960
- addEvent("dragEnd", `Dropped "${event.blockId}" at ${event.targetZone ?? "none"}`);
1961
- }
1962
- },
1963
- onBlockMove: (event) => {
1964
- const fromStr = `parent=${event.from.parentId ?? "root"}[${event.from.index}]`;
1965
- const toStr = `parent=${event.to.parentId ?? "root"}[${event.to.index}]`;
1966
- const ids = event.movedIds.length > 1 ? ` (${event.movedIds.length} blocks)` : "";
1967
- addEvent("blockMove", `Moved "${event.block.id}" from ${fromStr} to ${toStr}${ids}`);
1968
- },
1969
- onExpandChange: (event) => {
1970
- addEvent("expandChange", `${event.expanded ? "Expanded" : "Collapsed"} "${event.blockId}"`);
1971
- },
1972
- onHoverChange: (event) => {
1973
- if (event.zoneId) {
1974
- addEvent("hoverChange", `Hovering over zone "${event.zoneId}"`);
1975
- }
1976
- }
1977
- }), [addEvent]);
1978
- return { callbacks, events, clearEvents };
1979
- }
1980
- function DevToolsLogo({ size = 20 }) {
1981
- return /* @__PURE__ */ jsxRuntime.jsxs(
1982
- "svg",
1983
- {
1984
- width: size,
1985
- height: size,
1986
- viewBox: "0 0 24 24",
1987
- fill: "none",
1988
- xmlns: "http://www.w3.org/2000/svg",
1989
- children: [
1990
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "8", height: "5", rx: "1", fill: "#3b82f6", opacity: "0.9" }),
1991
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "8", y: "10", width: "8", height: "5", rx: "1", fill: "#10b981", opacity: "0.9" }),
1992
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "18", width: "8", height: "5", rx: "1", fill: "#f59e0b", opacity: "0.9" }),
1993
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 7 L6 10 L8 10", stroke: "#888", strokeWidth: "1.5", fill: "none", opacity: "0.6" }),
1994
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 15 L12 18 L14 18", stroke: "#888", strokeWidth: "1.5", fill: "none", opacity: "0.6" })
1995
- ]
1996
- }
1997
- );
1998
- }
1999
- function computeDiffMap(prev, next) {
2000
- const prevMap = new Map(prev.map((b) => [b.id, b]));
2001
- const changeMap = /* @__PURE__ */ new Map();
2002
- for (const block of next) {
2003
- const prevBlock = prevMap.get(block.id);
2004
- if (!prevBlock) {
2005
- changeMap.set(block.id, "added");
2006
- } else if (prevBlock.parentId !== block.parentId || prevBlock.order !== block.order) {
2007
- changeMap.set(block.id, "moved");
2008
- } else {
2009
- changeMap.set(block.id, "unchanged");
2010
- }
2011
- }
2012
- return changeMap;
2013
- }
2014
- function buildDiffTree(blocks, changeMap) {
2015
- const result = [];
2016
- const byParent = /* @__PURE__ */ new Map();
2017
- for (const block of blocks) {
2018
- const key = block.parentId ?? null;
2019
- const list = byParent.get(key) ?? [];
2020
- list.push(block);
2021
- byParent.set(key, list);
2022
- }
2023
- function walk(parentId, depth) {
2024
- const children = byParent.get(parentId) ?? [];
2025
- children.sort((a, b) => {
2026
- const ao = a.order, bo = b.order;
2027
- if (typeof ao === "number" && typeof bo === "number") return ao - bo;
2028
- return String(ao) < String(bo) ? -1 : String(ao) > String(bo) ? 1 : 0;
2029
- });
2030
- for (const block of children) {
2031
- result.push({ block, changeType: changeMap.get(block.id) ?? "unchanged", depth });
2032
- walk(block.id, depth + 1);
2033
- }
2034
- }
2035
- walk(null, 0);
2036
- return result;
2037
- }
2038
- var TYPE_COLORS = {
2039
- dragStart: "#3b82f6",
2040
- dragEnd: "#10b981",
2041
- blockMove: "#f59e0b",
2042
- expandChange: "#8b5cf6",
2043
- hoverChange: "#6b7280"
2044
- };
2045
- var TYPE_LABELS = {
2046
- dragStart: "DRAG",
2047
- dragEnd: "DROP",
2048
- blockMove: "MOVE",
2049
- expandChange: "EXPAND",
2050
- hoverChange: "HOVER"
2051
- };
2052
- var TYPE_TOOLTIPS = {
2053
- dragStart: "onDragStart \u2014 a block was picked up",
2054
- dragEnd: "onDragEnd \u2014 a block was dropped or drag was cancelled",
2055
- blockMove: "onBlockMove \u2014 a block was reparented to a new position",
2056
- expandChange: "onExpandChange \u2014 a container was expanded or collapsed",
2057
- hoverChange: "onHoverChange \u2014 the pointer entered a drop zone"
2058
- };
2059
- var DEFAULT_WIDTH = 340;
2060
- var DEFAULT_HEIGHT = 420;
2061
- var DIFF_EXTRA_WIDTH = 300;
2062
- var MIN_WIDTH = 280;
2063
- var MIN_HEIGHT = 200;
2064
- function getAnchorCSS(position) {
2065
- switch (position) {
2066
- case "bottom-right":
2067
- return { bottom: 16, right: 16 };
2068
- case "top-left":
2069
- return { top: 16, left: 16 };
2070
- case "top-right":
2071
- return { top: 16, right: 16 };
2072
- case "bottom-left":
2073
- default:
2074
- return { bottom: 16, left: 16 };
2075
- }
2076
- }
2077
- function computeCardOrigin(position, width, height) {
2078
- const vw = typeof window !== "undefined" ? window.innerWidth : 1024;
2079
- const vh = typeof window !== "undefined" ? window.innerHeight : 768;
2080
- switch (position) {
2081
- case "bottom-right":
2082
- return { x: Math.max(0, vw - 16 - width), y: Math.max(0, vh - 16 - height - 48) };
2083
- case "top-left":
2084
- return { x: 16, y: 16 + 48 };
2085
- case "top-right":
2086
- return { x: Math.max(0, vw - 16 - width), y: 16 + 48 };
2087
- case "bottom-left":
2088
- default:
2089
- return { x: 16, y: Math.max(0, vh - 16 - height - 48) };
2090
- }
2091
- }
2092
- function BlockTreeDevTools({
2093
- blocks,
2094
- containerTypes = [],
2095
- events,
2096
- onClearEvents,
2097
- getLabel = (b) => b.type,
2098
- initialOpen = false,
2099
- position = "bottom-left",
2100
- buttonStyle,
2101
- panelStyle
2102
- }) {
2103
- const [isOpen, setIsOpen] = react.useState(initialOpen);
2104
- const [showDiff, setShowDiff] = react.useState(false);
2105
- const [cardPos, setCardPos] = react.useState(null);
2106
- const [cardSize, setCardSize] = react.useState({ w: DEFAULT_WIDTH, h: DEFAULT_HEIGHT });
2107
- const [showTooltip, setShowTooltip] = react.useState(false);
2108
- const dragRef = react.useRef({
2109
- dragging: false,
2110
- startX: 0,
2111
- startY: 0,
2112
- origX: 0,
2113
- origY: 0
2114
- });
2115
- const resizeRef = react.useRef({ active: false, edge: "", startX: 0, startY: 0, origX: 0, origY: 0, origW: 0, origH: 0 });
2116
- const cardRef = react.useRef(null);
2117
- const prevBlocksRef = react.useRef(blocks);
2118
- const diffChangeMap = react.useMemo(() => computeDiffMap(prevBlocksRef.current, blocks), [blocks]);
2119
- react.useEffect(() => {
2120
- prevBlocksRef.current = blocks;
2121
- }, [blocks]);
2122
- const diffTree = react.useMemo(() => buildDiffTree(blocks, diffChangeMap), [blocks, diffChangeMap]);
2123
- const diffStats = react.useMemo(() => {
2124
- let added = 0, moved = 0;
2125
- for (const { changeType } of diffTree) {
2126
- if (changeType === "added") added++;
2127
- if (changeType === "moved") moved++;
2128
- }
2129
- return { added, moved };
2130
- }, [diffTree]);
2131
- react.useEffect(() => {
2132
- setCardSize((prev) => {
2133
- const targetW = showDiff ? DEFAULT_WIDTH + DIFF_EXTRA_WIDTH : DEFAULT_WIDTH;
2134
- const wasDefault = Math.abs(prev.w - DEFAULT_WIDTH) < 20;
2135
- const wasExpanded = Math.abs(prev.w - (DEFAULT_WIDTH + DIFF_EXTRA_WIDTH)) < 20;
2136
- if (showDiff && (wasDefault || prev.w < targetW)) {
2137
- return { ...prev, w: targetW };
2138
- }
2139
- if (!showDiff && wasExpanded) {
2140
- return { ...prev, w: DEFAULT_WIDTH };
2141
- }
2142
- return prev;
2143
- });
2144
- }, [showDiff]);
2145
- const getDefaultCardPos = react.useCallback(() => {
2146
- return computeCardOrigin(position, cardSize.w, cardSize.h);
2147
- }, [position, cardSize.w, cardSize.h]);
2148
- react.useEffect(() => {
2149
- if (isOpen && !cardPos) {
2150
- setCardPos(getDefaultCardPos());
2151
- }
2152
- }, [isOpen, cardPos, getDefaultCardPos]);
2153
- const handleDragPointerDown = react.useCallback((e) => {
2154
- e.preventDefault();
2155
- dragRef.current = {
2156
- dragging: true,
2157
- startX: e.clientX,
2158
- startY: e.clientY,
2159
- origX: cardPos?.x ?? 0,
2160
- origY: cardPos?.y ?? 0
2161
- };
2162
- e.target.setPointerCapture(e.pointerId);
2163
- }, [cardPos]);
2164
- const handleDragPointerMove = react.useCallback((e) => {
2165
- if (!dragRef.current.dragging) return;
2166
- const dx = e.clientX - dragRef.current.startX;
2167
- const dy = e.clientY - dragRef.current.startY;
2168
- const newX = dragRef.current.origX + dx;
2169
- const newY = dragRef.current.origY + dy;
2170
- const maxX = window.innerWidth - cardSize.w;
2171
- const maxY = window.innerHeight - 40;
2172
- setCardPos({
2173
- x: Math.max(0, Math.min(newX, maxX)),
2174
- y: Math.max(0, Math.min(newY, maxY))
2175
- });
2176
- }, [cardSize.w]);
2177
- const handleDragPointerUp = react.useCallback(() => {
2178
- dragRef.current.dragging = false;
2179
- }, []);
2180
- const handleResizePointerDown = react.useCallback((edge) => (e) => {
2181
- e.preventDefault();
2182
- e.stopPropagation();
2183
- resizeRef.current = {
2184
- active: true,
2185
- edge,
2186
- startX: e.clientX,
2187
- startY: e.clientY,
2188
- origX: cardPos?.x ?? 0,
2189
- origY: cardPos?.y ?? 0,
2190
- origW: cardSize.w,
2191
- origH: cardSize.h
2192
- };
2193
- e.target.setPointerCapture(e.pointerId);
2194
- }, [cardPos, cardSize]);
2195
- const handleResizePointerMove = react.useCallback((e) => {
2196
- const r = resizeRef.current;
2197
- if (!r.active) return;
2198
- const dx = e.clientX - r.startX;
2199
- const dy = e.clientY - r.startY;
2200
- let newW = r.origW;
2201
- let newH = r.origH;
2202
- let newX = r.origX;
2203
- let newY = r.origY;
2204
- if (r.edge.includes("e")) newW = Math.max(MIN_WIDTH, r.origW + dx);
2205
- if (r.edge.includes("w")) {
2206
- newW = Math.max(MIN_WIDTH, r.origW - dx);
2207
- newX = r.origX + (r.origW - newW);
2208
- }
2209
- if (r.edge.includes("s")) newH = Math.max(MIN_HEIGHT, r.origH + dy);
2210
- if (r.edge.includes("n")) {
2211
- newH = Math.max(MIN_HEIGHT, r.origH - dy);
2212
- newY = r.origY + (r.origH - newH);
2213
- }
2214
- setCardSize({ w: newW, h: newH });
2215
- setCardPos({ x: newX, y: newY });
2216
- }, []);
2217
- const handleResizePointerUp = react.useCallback(() => {
2218
- resizeRef.current.active = false;
2219
- }, []);
2220
- const toggle = react.useCallback(() => {
2221
- setIsOpen((prev) => !prev);
2222
- }, []);
2223
- const toggleDiff = react.useCallback(() => {
2224
- setShowDiff((prev) => !prev);
2225
- }, []);
2226
- const renderCountRef = react.useRef(0);
2227
- const lastRenderTimeRef = react.useRef(performance.now());
2228
- const prevBlockCountRef = react.useRef(blocks.length);
2229
- renderCountRef.current++;
2230
- const renderTime = performance.now() - lastRenderTimeRef.current;
2231
- lastRenderTimeRef.current = performance.now();
2232
- const blockCountDelta = blocks.length - prevBlockCountRef.current;
2233
- react.useEffect(() => {
2234
- prevBlockCountRef.current = blocks.length;
2235
- }, [blocks.length]);
2236
- const treeStats = react.useMemo(() => {
2237
- const index = computeNormalizedIndex(blocks);
2238
- const containers = blocks.filter((b) => containerTypes.includes(b.type));
2239
- let maxDepthVal = 0;
2240
- for (const block of blocks) {
2241
- const d = getBlockDepth(index, block.id);
2242
- if (d > maxDepthVal) maxDepthVal = d;
2243
- }
2244
- const validation = validateBlockTree(index);
2245
- return {
2246
- blockCount: blocks.length,
2247
- containerCount: containers.length,
2248
- maxDepth: maxDepthVal,
2249
- validation
2250
- };
2251
- }, [blocks, containerTypes]);
2252
- const formatTime = (ts) => {
2253
- const d = new Date(ts);
2254
- return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}`;
2255
- };
2256
- const anchor = getAnchorCSS(position);
2257
- const triggerBtnStyle = {
2258
- position: "fixed",
2259
- ...anchor,
2260
- zIndex: 99998,
2261
- width: 40,
2262
- height: 40,
2263
- borderRadius: "50%",
2264
- border: "none",
2265
- background: isOpen ? "rgba(59, 130, 246, 0.9)" : "rgba(30, 30, 30, 0.85)",
2266
- color: "#fff",
2267
- cursor: "pointer",
2268
- display: "flex",
2269
- alignItems: "center",
2270
- justifyContent: "center",
2271
- boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
2272
- transition: "background 0.15s, transform 0.15s",
2273
- ...buttonStyle
2274
- };
2275
- const tooltipStyle = {
2276
- position: "absolute",
2277
- whiteSpace: "nowrap",
2278
- fontSize: 11,
2279
- fontWeight: 500,
2280
- padding: "4px 10px",
2281
- borderRadius: 6,
2282
- background: "rgba(15, 15, 20, 0.95)",
2283
- color: "#ccc",
2284
- boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
2285
- pointerEvents: "none",
2286
- // Position based on anchor corner
2287
- ...anchor.bottom !== void 0 ? { bottom: "100%", marginBottom: 8 } : { top: "100%", marginTop: 8 },
2288
- ...anchor.left !== void 0 ? { left: 0 } : { right: 0 }
2289
- };
2290
- const cardStyle = {
2291
- position: "fixed",
2292
- left: cardPos?.x ?? 0,
2293
- top: cardPos?.y ?? 0,
2294
- zIndex: 99999,
2295
- width: cardSize.w,
2296
- height: cardSize.h,
2297
- background: "rgba(20, 20, 24, 0.95)",
2298
- backdropFilter: "blur(12px)",
2299
- border: "1px solid rgba(255,255,255,0.1)",
2300
- borderRadius: 10,
2301
- color: "#e0e0e0",
2302
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
2303
- fontSize: 12,
2304
- display: "flex",
2305
- flexDirection: "column",
2306
- boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
2307
- overflow: "hidden",
2308
- ...panelStyle
2309
- };
2310
- const titleBarStyle = {
2311
- display: "flex",
2312
- alignItems: "center",
2313
- justifyContent: "space-between",
2314
- padding: "8px 12px",
2315
- gap: 8,
2316
- background: "rgba(255,255,255,0.04)",
2317
- borderBottom: "1px solid rgba(255,255,255,0.08)",
2318
- cursor: "grab",
2319
- userSelect: "none",
2320
- flexShrink: 0,
2321
- touchAction: "none"
2322
- };
2323
- const titleTextStyle = {
2324
- fontSize: 11,
2325
- fontWeight: 600,
2326
- letterSpacing: "0.03em",
2327
- opacity: 0.8,
2328
- flexShrink: 0
2329
- };
2330
- const titleBtnStyle = (active) => ({
2331
- height: 22,
2332
- padding: "0 8px",
2333
- borderRadius: 4,
2334
- border: "1px solid " + (active ? "rgba(59,130,246,0.5)" : "rgba(128,128,128,0.25)"),
2335
- background: active ? "rgba(59,130,246,0.15)" : "transparent",
2336
- color: active ? "#93b8f7" : "#999",
2337
- cursor: "pointer",
2338
- fontSize: 10,
2339
- fontWeight: 600,
2340
- letterSpacing: "0.02em",
2341
- flexShrink: 0
2342
- });
2343
- const closeBtnStyle = {
2344
- width: 20,
2345
- height: 20,
2346
- borderRadius: 4,
2347
- border: "none",
2348
- background: "transparent",
2349
- color: "#999",
2350
- cursor: "pointer",
2351
- display: "flex",
2352
- alignItems: "center",
2353
- justifyContent: "center",
2354
- fontSize: 14,
2355
- lineHeight: "1",
2356
- padding: 0,
2357
- flexShrink: 0
2358
- };
2359
- const bodyStyle = {
2360
- flex: 1,
2361
- display: "flex",
2362
- flexDirection: "row",
2363
- overflow: "hidden",
2364
- minHeight: 0
2365
- };
2366
- const mainColumnStyle = {
2367
- flex: showDiff ? "0 0 50%" : "1 1 100%",
2368
- overflow: "auto",
2369
- padding: "10px 12px",
2370
- minWidth: 0
2371
- };
2372
- const diffColumnStyle = {
2373
- flex: "0 0 50%",
2374
- overflow: "auto",
2375
- padding: "10px 12px",
2376
- borderLeft: "1px solid rgba(255,255,255,0.08)",
2377
- minWidth: 0
2378
- };
2379
- const sectionStyle = {
2380
- marginBottom: 8
2381
- };
2382
- const headingStyle = {
2383
- fontSize: 11,
2384
- fontWeight: 600,
2385
- textTransform: "uppercase",
2386
- letterSpacing: "0.05em",
2387
- opacity: 0.6,
2388
- marginBottom: 6
2389
- };
2390
- const statRowStyle = {
2391
- display: "flex",
2392
- justifyContent: "space-between",
2393
- alignItems: "center",
2394
- fontSize: 12,
2395
- padding: "2px 0"
2396
- };
2397
- const statValueStyle = {
2398
- fontFamily: "monospace",
2399
- fontSize: 11,
2400
- opacity: 0.8
2401
- };
2402
- const badgeStyle = (color) => ({
2403
- display: "inline-block",
2404
- fontSize: 9,
2405
- fontWeight: 700,
2406
- fontFamily: "monospace",
2407
- padding: "1px 5px",
2408
- borderRadius: 3,
2409
- backgroundColor: color + "22",
2410
- color,
2411
- marginRight: 6,
2412
- flexShrink: 0
2413
- });
2414
- const eventListStyle = {
2415
- maxHeight: 160,
2416
- overflowY: "auto",
2417
- fontSize: 11,
2418
- lineHeight: "1.6"
2419
- };
2420
- const eventItemStyle = {
2421
- display: "flex",
2422
- alignItems: "flex-start",
2423
- gap: 4,
2424
- padding: "2px 0",
2425
- borderBottom: "1px solid rgba(128,128,128,0.1)"
2426
- };
2427
- const timeStyle = {
2428
- fontFamily: "monospace",
2429
- fontSize: 10,
2430
- opacity: 0.4,
2431
- flexShrink: 0,
2432
- minWidth: 52
2433
- };
2434
- const clearBtnStyle = {
2435
- fontSize: 10,
2436
- padding: "2px 8px",
2437
- border: "1px solid rgba(128,128,128,0.3)",
2438
- borderRadius: 4,
2439
- background: "transparent",
2440
- color: "#ccc",
2441
- cursor: "pointer",
2442
- opacity: 0.7
2443
- };
2444
- const EDGE_SIZE = 6;
2445
- const resizeEdge = (edge) => {
2446
- const base = {
2447
- position: "absolute",
2448
- zIndex: 1
2449
- };
2450
- const cursors = {
2451
- n: "ns-resize",
2452
- s: "ns-resize",
2453
- e: "ew-resize",
2454
- w: "ew-resize",
2455
- ne: "nesw-resize",
2456
- nw: "nwse-resize",
2457
- se: "nwse-resize",
2458
- sw: "nesw-resize"
2459
- };
2460
- const styles = {
2461
- n: { top: 0, left: EDGE_SIZE, right: EDGE_SIZE, height: EDGE_SIZE },
2462
- s: { bottom: 0, left: EDGE_SIZE, right: EDGE_SIZE, height: EDGE_SIZE },
2463
- e: { right: 0, top: EDGE_SIZE, bottom: EDGE_SIZE, width: EDGE_SIZE },
2464
- w: { left: 0, top: EDGE_SIZE, bottom: EDGE_SIZE, width: EDGE_SIZE },
2465
- ne: { top: 0, right: 0, width: EDGE_SIZE * 2, height: EDGE_SIZE * 2 },
2466
- nw: { top: 0, left: 0, width: EDGE_SIZE * 2, height: EDGE_SIZE * 2 },
2467
- se: { bottom: 0, right: 0, width: EDGE_SIZE * 2, height: EDGE_SIZE * 2 },
2468
- sw: { bottom: 0, left: 0, width: EDGE_SIZE * 2, height: EDGE_SIZE * 2 }
2469
- };
2470
- return { ...base, cursor: cursors[edge], ...styles[edge] };
2471
- };
2472
- const EDGES = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
2473
- const diffRowColor = (ct) => {
2474
- if (ct === "added") return { background: "rgba(16,185,129,0.1)", color: "#34d399" };
2475
- if (ct === "moved") return { background: "rgba(245,158,11,0.1)", color: "#fbbf24" };
2476
- return { color: "rgba(200,200,200,0.5)" };
2477
- };
2478
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2479
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "fixed", ...anchor, zIndex: 99998 }, children: [
2480
- /* @__PURE__ */ jsxRuntime.jsx(
2481
- "button",
2482
- {
2483
- onClick: toggle,
2484
- onMouseEnter: () => setShowTooltip(true),
2485
- onMouseLeave: () => setShowTooltip(false),
2486
- style: triggerBtnStyle,
2487
- "aria-label": isOpen ? "Close DevTools" : "Open DevTools",
2488
- children: /* @__PURE__ */ jsxRuntime.jsx(DevToolsLogo, { size: 20 })
2489
- }
2490
- ),
2491
- showTooltip && !isOpen && /* @__PURE__ */ jsxRuntime.jsx("div", { style: tooltipStyle, children: "dnd-block-tree DevTools" })
2492
- ] }),
2493
- isOpen && cardPos && /* @__PURE__ */ jsxRuntime.jsxs(
2494
- "div",
2495
- {
2496
- ref: cardRef,
2497
- style: cardStyle,
2498
- "data-devtools-root": "",
2499
- children: [
2500
- EDGES.map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
2501
- "div",
2502
- {
2503
- style: resizeEdge(edge),
2504
- onPointerDown: handleResizePointerDown(edge),
2505
- onPointerMove: handleResizePointerMove,
2506
- onPointerUp: handleResizePointerUp
2507
- },
2508
- edge
2509
- )),
2510
- /* @__PURE__ */ jsxRuntime.jsxs(
2511
- "div",
2512
- {
2513
- style: titleBarStyle,
2514
- onPointerDown: handleDragPointerDown,
2515
- onPointerMove: handleDragPointerMove,
2516
- onPointerUp: handleDragPointerUp,
2517
- children: [
2518
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: titleTextStyle, children: "DevTools" }),
2519
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
2520
- /* @__PURE__ */ jsxRuntime.jsx(
2521
- "button",
2522
- {
2523
- onClick: toggleDiff,
2524
- style: titleBtnStyle(showDiff),
2525
- onPointerDown: (e) => e.stopPropagation(),
2526
- title: "Toggle structure diff \u2014 shows which blocks were added, moved, or unchanged",
2527
- children: "Diff"
2528
- }
2529
- ),
2530
- /* @__PURE__ */ jsxRuntime.jsx(
2531
- "button",
2532
- {
2533
- onClick: toggle,
2534
- style: closeBtnStyle,
2535
- onPointerDown: (e) => e.stopPropagation(),
2536
- "aria-label": "Close DevTools",
2537
- children: "\xD7"
2538
- }
2539
- )
2540
- ]
2541
- }
2542
- ),
2543
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: bodyStyle, children: [
2544
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: mainColumnStyle, children: [
2545
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: sectionStyle, "data-devtools-section": "tree-state", children: [
2546
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: headingStyle, title: "Live snapshot of the block tree structure", children: "Tree State" }),
2547
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: statRowStyle, title: "Total number of blocks in the flat array", children: [
2548
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Blocks" }),
2549
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: statValueStyle, children: treeStats.blockCount })
2550
- ] }),
2551
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: statRowStyle, title: "Blocks whose type is in containerTypes (can have children)", children: [
2552
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Containers" }),
2553
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: statValueStyle, children: treeStats.containerCount })
2554
- ] }),
2555
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: statRowStyle, title: "Deepest nesting level in the tree (root = 1)", children: [
2556
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Max Depth" }),
2557
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: statValueStyle, children: treeStats.maxDepth })
2558
- ] }),
2559
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: statRowStyle, title: "Checks for cycles, orphans, and stale parent refs", children: [
2560
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Validation" }),
2561
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
2562
- ...statValueStyle,
2563
- color: treeStats.validation.valid ? "#10b981" : "#ef4444"
2564
- }, children: treeStats.validation.valid ? "Valid" : `${treeStats.validation.issues.length} issue(s)` })
2565
- ] }),
2566
- !treeStats.validation.valid && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 10, color: "#ef4444", marginTop: 4 }, children: treeStats.validation.issues.map((issue, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: issue }, i)) })
2567
- ] }),
2568
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: sectionStyle, "data-devtools-section": "event-log", children: [
2569
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }, children: [
2570
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headingStyle, title: "Chronological log of drag, drop, move, expand, and hover events", children: [
2571
- "Event Log (",
2572
- events.length,
2573
- ")"
2574
- ] }),
2575
- events.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onClearEvents, style: clearBtnStyle, children: "Clear" })
2576
- ] }),
2577
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: eventListStyle, children: events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, opacity: 0.4, padding: "8px 0" }, children: "No events yet. Drag some blocks!" }) : events.map((event) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: eventItemStyle, children: [
2578
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: timeStyle, children: formatTime(event.timestamp) }),
2579
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: badgeStyle(TYPE_COLORS[event.type]), title: TYPE_TOOLTIPS[event.type], children: TYPE_LABELS[event.type] }),
2580
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { wordBreak: "break-word" }, children: event.summary })
2581
- ] }, event.id)) })
2582
- ] }),
2583
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: sectionStyle, "data-devtools-section": "performance", children: [
2584
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: headingStyle, title: "Render metrics for the DevTools component itself", children: "Performance" }),
2585
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: statRowStyle, title: "How many times this DevTools component has rendered", children: [
2586
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Render Count" }),
2587
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: statValueStyle, children: renderCountRef.current })
2588
- ] }),
2589
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: statRowStyle, title: "Time since the previous render of this component", children: [
2590
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Last Render" }),
2591
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: statValueStyle, children: [
2592
- renderTime.toFixed(1),
2593
- "ms"
2594
- ] })
2595
- ] }),
2596
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: statRowStyle, title: "Change in block count since the last render (+added / -removed)", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: {
2597
- ...statValueStyle,
2598
- color: blockCountDelta > 0 ? "#10b981" : blockCountDelta < 0 ? "#ef4444" : void 0
2599
- }, children: [
2600
- blockCountDelta > 0 ? "+" : "",
2601
- blockCountDelta
2602
- ] }) })
2603
- ] })
2604
- ] }),
2605
- showDiff && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: diffColumnStyle, children: [
2606
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }, children: [
2607
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: headingStyle, title: "Tree structure diff \u2014 compares current state to the previous render", children: "Structure" }),
2608
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 10, fontSize: 11, fontWeight: 500 }, children: [
2609
- diffStats.added > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "#34d399", display: "flex", alignItems: "center", gap: 4 }, children: [
2610
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: 5, height: 5, borderRadius: "50%", background: "#34d399", display: "inline-block" } }),
2611
- diffStats.added,
2612
- " new"
2613
- ] }),
2614
- diffStats.moved > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "#fbbf24", display: "flex", alignItems: "center", gap: 4 }, children: [
2615
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: 5, height: 5, borderRadius: "50%", background: "#fbbf24", display: "inline-block" } }),
2616
- diffStats.moved,
2617
- " moved"
2618
- ] }),
2619
- diffStats.added === 0 && diffStats.moved === 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.4 }, children: "No changes" })
2620
- ] })
2621
- ] }),
2622
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontFamily: "monospace", fontSize: 11, lineHeight: "1.7" }, children: diffTree.map(({ block, changeType, depth }) => /* @__PURE__ */ jsxRuntime.jsxs(
2623
- "div",
2624
- {
2625
- style: {
2626
- paddingLeft: depth * 14,
2627
- padding: "2px 6px 2px " + (depth * 14 + 6) + "px",
2628
- borderRadius: 4,
2629
- display: "flex",
2630
- alignItems: "center",
2631
- gap: 6,
2632
- ...diffRowColor(changeType)
2633
- },
2634
- children: [
2635
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { width: 12, textAlign: "center", fontWeight: 700, fontSize: 10 }, children: [
2636
- changeType === "added" && "+",
2637
- changeType === "moved" && "~"
2638
- ] }),
2639
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
2640
- textTransform: "uppercase",
2641
- fontSize: 9,
2642
- letterSpacing: "0.05em",
2643
- width: 50,
2644
- flexShrink: 0,
2645
- opacity: changeType === "unchanged" ? 0.5 : 1
2646
- }, children: block.type }),
2647
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
2648
- overflow: "hidden",
2649
- textOverflow: "ellipsis",
2650
- whiteSpace: "nowrap",
2651
- flex: 1,
2652
- minWidth: 0
2653
- }, children: getLabel(block) }),
2654
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
2655
- fontFamily: "monospace",
2656
- fontSize: 10,
2657
- flexShrink: 0,
2658
- padding: "1px 5px",
2659
- borderRadius: 3,
2660
- ...changeType === "added" ? { background: "rgba(16,185,129,0.2)", color: "#34d399" } : changeType === "moved" ? { background: "rgba(245,158,11,0.2)", color: "#fbbf24" } : { background: "rgba(128,128,128,0.15)", color: "rgba(200,200,200,0.4)" }
2661
- }, children: String(block.order) })
2662
- ]
2663
- },
2664
- block.id
2665
- )) })
2666
- ] })
2667
- ] })
2668
- ]
2669
- }
2670
- )
2671
- ] });
2672
- }
2673
-
2674
- // src/utils/serialization.ts
2675
- function flatToNested(blocks) {
2676
- const byParent = /* @__PURE__ */ new Map();
2677
- for (const block of blocks) {
2678
- const key = block.parentId ?? null;
2679
- const list = byParent.get(key);
2680
- if (list) {
2681
- list.push(block);
2682
- } else {
2683
- byParent.set(key, [block]);
2684
- }
2685
- }
2686
- for (const list of byParent.values()) {
2687
- list.sort((a, b) => {
2688
- if (typeof a.order === "string" && typeof b.order === "string") {
2689
- return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
2690
- }
2691
- return Number(a.order) - Number(b.order);
2692
- });
2693
- }
2694
- function buildChildren(parentId) {
2695
- const siblings = byParent.get(parentId) ?? [];
2696
- return siblings.map((block) => {
2697
- const { parentId: _p, order: _o, ...rest } = block;
2698
- return {
2699
- ...rest,
2700
- children: buildChildren(block.id)
2701
- };
2702
- });
2703
- }
2704
- return buildChildren(null);
2705
- }
2706
- function nestedToFlat(nested) {
2707
- const result = [];
2708
- function walk(nodes, parentId) {
2709
- for (let i = 0; i < nodes.length; i++) {
2710
- const { children, ...rest } = nodes[i];
2711
- result.push({
2712
- ...rest,
2713
- parentId,
2714
- order: i
2715
- });
2716
- walk(children, rest.id);
2717
- }
2718
- }
2719
- walk(nested, null);
2720
- return result;
2721
- }
2722
-
2723
- exports.BlockTree = BlockTree;
2724
- exports.BlockTreeDevTools = BlockTreeDevTools;
2725
- exports.BlockTreeSSR = BlockTreeSSR;
2726
- exports.DragOverlay = DragOverlay;
2727
- exports.DropZone = DropZone;
2728
- exports.TreeRenderer = TreeRenderer;
2729
- exports.buildOrderedBlocks = buildOrderedBlocks;
2730
- exports.cloneMap = cloneMap;
2731
- exports.cloneParentMap = cloneParentMap;
2732
- exports.closestCenterCollision = closestCenterCollision;
2733
- exports.compareFractionalKeys = compareFractionalKeys;
2734
- exports.computeNormalizedIndex = computeNormalizedIndex;
2735
- exports.createBlockState = createBlockState;
2736
- exports.createStickyCollision = createStickyCollision;
2737
- exports.createTreeState = createTreeState;
2738
- exports.debounce = debounce;
2739
- exports.deleteBlockAndDescendants = deleteBlockAndDescendants;
2740
- exports.extractBlockId = extractBlockId;
2741
- exports.extractUUID = extractUUID;
2742
- exports.flatToNested = flatToNested;
2743
- exports.generateId = generateId;
2744
- exports.generateInitialKeys = generateInitialKeys;
2745
- exports.generateKeyBetween = generateKeyBetween;
2746
- exports.generateNKeysBetween = generateNKeysBetween;
2747
- exports.getBlockDepth = getBlockDepth;
2748
- exports.getDescendantIds = getDescendantIds;
2749
- exports.getDropZoneType = getDropZoneType;
2750
- exports.getSensorConfig = getSensorConfig;
2751
- exports.getSubtreeDepth = getSubtreeDepth;
2752
- exports.initFractionalOrder = initFractionalOrder;
2753
- exports.nestedToFlat = nestedToFlat;
2754
- exports.reparentBlockIndex = reparentBlockIndex;
2755
- exports.reparentMultipleBlocks = reparentMultipleBlocks;
2756
- exports.triggerHaptic = triggerHaptic;
2757
- exports.useBlockHistory = useBlockHistory;
2758
- exports.useConfiguredSensors = useConfiguredSensors;
2759
- exports.useDevToolsCallbacks = useDevToolsCallbacks;
2760
- exports.useLayoutAnimation = useLayoutAnimation;
2761
- exports.useVirtualTree = useVirtualTree;
2762
- exports.validateBlockTree = validateBlockTree;
2763
- exports.weightedVerticalCollision = weightedVerticalCollision;
7
+ Object.keys(react).forEach(function (k) {
8
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
9
+ enumerable: true,
10
+ get: function () { return react[k]; }
11
+ });
12
+ });
2764
13
  //# sourceMappingURL=index.js.map
2765
14
  //# sourceMappingURL=index.js.map