react-headless-dock-layout 0.1.0 → 0.1.1

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 ADDED
@@ -0,0 +1,894 @@
1
+ // src/internal/assertNever.ts
2
+ function assertNever(value) {
3
+ throw new Error(`Unexpected value: ${value}`);
4
+ }
5
+
6
+ // src/internal/findParentNode.ts
7
+ function findParentNode(root, id) {
8
+ if (root === null) {
9
+ return null;
10
+ }
11
+ return findParentNodeInSubTree(id, root);
12
+ }
13
+ function findParentNodeInSubTree(id, node) {
14
+ if (node.type === "panel") {
15
+ return null;
16
+ } else if (node.type === "split") {
17
+ if (node.left.id === id || node.right.id === id) {
18
+ return node;
19
+ }
20
+ return findParentNodeInSubTree(id, node.left) ?? findParentNodeInSubTree(id, node.right);
21
+ } else {
22
+ assertNever(node);
23
+ }
24
+ }
25
+
26
+ // src/strategies.ts
27
+ var evenlyDividedHorizontalStrategy = {
28
+ getPlacement(root) {
29
+ const horizontalNodeCount = calculateHorizontalSplitCount(root) + 1;
30
+ return {
31
+ targetId: root.id,
32
+ direction: "right",
33
+ ratio: horizontalNodeCount / (horizontalNodeCount + 1)
34
+ };
35
+ }
36
+ };
37
+ function calculateHorizontalSplitCount(node) {
38
+ if (node.type === "panel") {
39
+ return 0;
40
+ } else if (node.type === "split") {
41
+ if (node.orientation === "horizontal") {
42
+ return 1 + calculateHorizontalSplitCount(node.left) + calculateHorizontalSplitCount(node.right);
43
+ } else if (node.orientation === "vertical") {
44
+ return 0 + calculateHorizontalSplitCount(node.left) + calculateHorizontalSplitCount(node.right);
45
+ } else {
46
+ assertNever(node.orientation);
47
+ }
48
+ } else {
49
+ assertNever(node);
50
+ }
51
+ }
52
+ var bspStrategy = {
53
+ getPlacement(root) {
54
+ const rightMostPanel = findRightMostPanel(root);
55
+ const parentNode = findParentNode(root, rightMostPanel.id);
56
+ return {
57
+ targetId: rightMostPanel.id,
58
+ direction: parentNode === null ? "right" : parentNode.orientation === "horizontal" ? "bottom" : "right",
59
+ ratio: 0.5
60
+ };
61
+ }
62
+ };
63
+ function findRightMostPanel(node) {
64
+ if (node.type === "panel") {
65
+ return node;
66
+ } else if (node.type === "split") {
67
+ return findRightMostPanel(node.right);
68
+ } else {
69
+ assertNever(node);
70
+ }
71
+ }
72
+
73
+ // src/useDockLayout.ts
74
+ import {
75
+ useEffect,
76
+ useLayoutEffect,
77
+ useRef,
78
+ useState,
79
+ useSyncExternalStore
80
+ } from "react";
81
+
82
+ // src/internal/invariant.tsx
83
+ function invariant(condition, message) {
84
+ if (!condition) {
85
+ throw new Error(message ?? "Invariant failed");
86
+ }
87
+ }
88
+
89
+ // src/internal/clamp.tsx
90
+ function clamp(value, min, max) {
91
+ return Math.max(min, Math.min(max, value));
92
+ }
93
+
94
+ // src/internal/EventEmitter.ts
95
+ var EventEmitter = class {
96
+ _listeners = /* @__PURE__ */ new Set();
97
+ subscribe(listener) {
98
+ this._listeners.add(listener);
99
+ return () => {
100
+ this._listeners.delete(listener);
101
+ };
102
+ }
103
+ emit() {
104
+ this._listeners.forEach((listener) => {
105
+ listener();
106
+ });
107
+ }
108
+ };
109
+
110
+ // src/internal/LayoutManager/calculateLayoutRects.ts
111
+ function calculateLayoutRects(root, options) {
112
+ if (root === null) {
113
+ return [];
114
+ }
115
+ const rects = [];
116
+ const traverse = (node, rect) => {
117
+ if (node.type === "split") {
118
+ if (node.orientation === "horizontal") {
119
+ rects.push({
120
+ id: node.id,
121
+ type: "split",
122
+ orientation: node.orientation,
123
+ x: Math.round(rect.x + rect.width * node.ratio - options.gap / 2),
124
+ y: Math.round(rect.y),
125
+ width: Math.round(options.gap),
126
+ height: Math.round(rect.height)
127
+ });
128
+ traverse(node.left, {
129
+ x: rect.x,
130
+ y: rect.y,
131
+ width: rect.width * node.ratio - options.gap / 2,
132
+ height: rect.height
133
+ });
134
+ traverse(node.right, {
135
+ x: rect.x + rect.width * node.ratio + options.gap / 2,
136
+ y: rect.y,
137
+ width: rect.width * (1 - node.ratio) - options.gap / 2,
138
+ height: rect.height
139
+ });
140
+ } else if (node.orientation === "vertical") {
141
+ rects.push({
142
+ id: node.id,
143
+ type: "split",
144
+ orientation: node.orientation,
145
+ x: Math.round(rect.x),
146
+ y: Math.round(rect.y + rect.height * node.ratio - options.gap / 2),
147
+ width: Math.round(rect.width),
148
+ height: Math.round(options.gap)
149
+ });
150
+ traverse(node.left, {
151
+ x: rect.x,
152
+ y: rect.y,
153
+ width: rect.width,
154
+ height: rect.height * node.ratio - options.gap / 2
155
+ });
156
+ traverse(node.right, {
157
+ x: rect.x,
158
+ y: rect.y + rect.height * node.ratio + options.gap / 2,
159
+ width: rect.width,
160
+ height: rect.height * (1 - node.ratio) - options.gap / 2
161
+ });
162
+ } else {
163
+ assertNever(node.orientation);
164
+ }
165
+ } else if (node.type === "panel") {
166
+ rects.push({
167
+ id: node.id,
168
+ type: "panel",
169
+ x: Math.round(rect.x),
170
+ y: Math.round(rect.y),
171
+ width: Math.round(rect.width),
172
+ height: Math.round(rect.height)
173
+ });
174
+ } else {
175
+ assertNever(node);
176
+ }
177
+ };
178
+ traverse(root, {
179
+ x: 0,
180
+ y: 0,
181
+ width: options.size.width,
182
+ height: options.size.height
183
+ });
184
+ return rects;
185
+ }
186
+
187
+ // src/internal/LayoutManager/calculateMinSize.ts
188
+ function calculateMinSize(node, gap) {
189
+ if (node.type === "panel") {
190
+ return {
191
+ width: node.minSize?.width ?? 0,
192
+ height: node.minSize?.height ?? 0
193
+ };
194
+ } else if (node.type === "split") {
195
+ if (node.orientation === "horizontal") {
196
+ return {
197
+ width: calculateMinSize(node.left, gap).width + gap + calculateMinSize(node.right, gap).width,
198
+ height: Math.max(
199
+ calculateMinSize(node.left, gap).height,
200
+ calculateMinSize(node.right, gap).height
201
+ )
202
+ };
203
+ } else if (node.orientation === "vertical") {
204
+ return {
205
+ width: Math.max(
206
+ calculateMinSize(node.left, gap).width,
207
+ calculateMinSize(node.right, gap).width
208
+ ),
209
+ height: calculateMinSize(node.left, gap).height + gap + calculateMinSize(node.right, gap).height
210
+ };
211
+ } else {
212
+ assertNever(node.orientation);
213
+ }
214
+ } else {
215
+ assertNever(node);
216
+ }
217
+ }
218
+
219
+ // src/internal/LayoutManager/findClosestDirection.ts
220
+ function findClosestDirection(rect, point) {
221
+ const centerX = rect.x + rect.width / 2;
222
+ const centerY = rect.y + rect.height / 2;
223
+ const dx = (point.x - centerX) / (rect.width / 2);
224
+ const dy = (point.y - centerY) / (rect.height / 2);
225
+ if (Math.abs(dx) > Math.abs(dy)) {
226
+ return dx > 0 ? "right" : "left";
227
+ } else {
228
+ return dy > 0 ? "bottom" : "top";
229
+ }
230
+ }
231
+
232
+ // src/internal/LayoutManager/LayoutTree.ts
233
+ var LayoutTree = class {
234
+ _root = null;
235
+ constructor(root) {
236
+ this._root = root;
237
+ }
238
+ get root() {
239
+ return this._root;
240
+ }
241
+ set root(root) {
242
+ this._root = root;
243
+ }
244
+ findNode(id) {
245
+ if (this._root === null) {
246
+ return null;
247
+ }
248
+ return this.findNodeInSubTree(id, this._root);
249
+ }
250
+ findNodeInSubTree(id, node) {
251
+ if (id === node.id) {
252
+ return node;
253
+ }
254
+ if (node.type === "panel") {
255
+ return null;
256
+ } else if (node.type === "split") {
257
+ return this.findNodeInSubTree(id, node.left) ?? this.findNodeInSubTree(id, node.right);
258
+ } else {
259
+ assertNever(node);
260
+ }
261
+ }
262
+ findParentNode(id) {
263
+ return findParentNode(this._root, id);
264
+ }
265
+ replaceChildNode({
266
+ parentId,
267
+ oldChildId,
268
+ newChild
269
+ }) {
270
+ const parentNode = this.findNode(parentId);
271
+ if (parentNode === null) {
272
+ throw new Error(`Parent node with id ${parentId} not found`);
273
+ }
274
+ if (parentNode.type !== "split") {
275
+ throw new Error(`Parent node with id ${parentId} is not a split node`);
276
+ }
277
+ const oldChildNode = this.findNode(oldChildId);
278
+ if (oldChildNode === null) {
279
+ throw new Error(`Child node with id ${oldChildId} not found`);
280
+ }
281
+ if (parentNode.left.id === oldChildId) {
282
+ parentNode.left = newChild;
283
+ } else if (parentNode.right.id === oldChildId) {
284
+ parentNode.right = newChild;
285
+ } else {
286
+ throw new Error(
287
+ `Child node with id ${oldChildId} is not a child of the parent node with id ${parentId}`
288
+ );
289
+ }
290
+ }
291
+ };
292
+
293
+ // src/internal/LayoutManager/LayoutManager.ts
294
+ var LayoutManager = class {
295
+ _tree;
296
+ _options;
297
+ _eventEmitter = new EventEmitter();
298
+ _layoutRects = [];
299
+ _addPanelStrategy = evenlyDividedHorizontalStrategy;
300
+ constructor(root, options) {
301
+ this._tree = new LayoutTree(root);
302
+ this._options = {
303
+ gap: options?.gap ?? 10,
304
+ size: options?.size ?? { width: 0, height: 0 }
305
+ };
306
+ this._layoutRects = calculateLayoutRects(root, this._options);
307
+ }
308
+ get root() {
309
+ return this._tree.root;
310
+ }
311
+ set root(root) {
312
+ this._tree.root = root;
313
+ this.syncLayoutRects();
314
+ }
315
+ get layoutRects() {
316
+ return this._layoutRects;
317
+ }
318
+ subscribe = (listener) => {
319
+ const unsubscribe = this._eventEmitter.subscribe(listener);
320
+ return unsubscribe;
321
+ };
322
+ setSize(size) {
323
+ this._options.size = size;
324
+ this.syncLayoutRects();
325
+ }
326
+ removePanel(id) {
327
+ if (this._tree.root === null) {
328
+ throw new Error("Root node is null");
329
+ }
330
+ const node = this._tree.findNode(id);
331
+ if (node === null) {
332
+ throw new Error(`Node with id ${id} not found`);
333
+ }
334
+ if (node.type !== "panel") {
335
+ throw new Error(`Node with id ${id} is not a panel`);
336
+ }
337
+ if (node.id === this._tree.root.id) {
338
+ this._tree.root = null;
339
+ this.syncLayoutRects();
340
+ return;
341
+ }
342
+ const parentNode = this._tree.findParentNode(id);
343
+ invariant(parentNode !== null, "Parent node is not null");
344
+ const siblingNode = parentNode.left.id === node.id ? parentNode.right : parentNode.left;
345
+ if (parentNode.id === this._tree.root.id) {
346
+ this._tree.root = siblingNode;
347
+ this.syncLayoutRects();
348
+ return;
349
+ }
350
+ const grandParentNode = this._tree.findParentNode(parentNode.id);
351
+ invariant(grandParentNode !== null, "Grand parent node is not null");
352
+ this._tree.replaceChildNode({
353
+ parentId: grandParentNode.id,
354
+ oldChildId: parentNode.id,
355
+ newChild: siblingNode
356
+ });
357
+ this.syncLayoutRects();
358
+ }
359
+ movePanel({
360
+ sourceId,
361
+ targetId,
362
+ point
363
+ }) {
364
+ if (this._tree.root === null) {
365
+ throw new Error("Root node is null");
366
+ }
367
+ if (this._tree.root.type !== "split") {
368
+ throw new Error("Root node is not a split node");
369
+ }
370
+ const sourceNode = this._tree.findNode(sourceId);
371
+ if (sourceNode === null) {
372
+ throw new Error(`Node with id ${sourceId} not found`);
373
+ }
374
+ if (sourceNode.type !== "panel") {
375
+ throw new Error(`Node with id ${sourceId} is not a panel node`);
376
+ }
377
+ const sourceNodeParent = this._tree.findParentNode(sourceId);
378
+ invariant(sourceNodeParent !== null);
379
+ const targetNode = this._tree.findNode(targetId);
380
+ if (targetNode === null) {
381
+ throw new Error(`Node with id ${targetId} not found`);
382
+ }
383
+ if (targetNode.type !== "panel") {
384
+ throw new Error(`Node with id ${targetId} is not a panel node`);
385
+ }
386
+ const sourceNodeSibling = sourceNodeParent.left.id === sourceId ? sourceNodeParent.right : sourceNodeParent.left;
387
+ const targetRect = this.findRect(targetId);
388
+ invariant(targetRect !== null);
389
+ invariant(targetRect.type === "panel");
390
+ const direction = findClosestDirection(targetRect, point);
391
+ if (sourceNodeSibling.id === targetId) {
392
+ if (direction === "left") {
393
+ sourceNodeParent.orientation = "horizontal";
394
+ sourceNodeParent.left = sourceNode;
395
+ sourceNodeParent.right = targetNode;
396
+ } else if (direction === "right") {
397
+ sourceNodeParent.orientation = "horizontal";
398
+ sourceNodeParent.left = targetNode;
399
+ sourceNodeParent.right = sourceNode;
400
+ } else if (direction === "top") {
401
+ sourceNodeParent.orientation = "vertical";
402
+ sourceNodeParent.left = sourceNode;
403
+ sourceNodeParent.right = targetNode;
404
+ } else if (direction === "bottom") {
405
+ sourceNodeParent.orientation = "vertical";
406
+ sourceNodeParent.left = targetNode;
407
+ sourceNodeParent.right = sourceNode;
408
+ } else {
409
+ assertNever(direction);
410
+ }
411
+ this.syncLayoutRects();
412
+ return;
413
+ }
414
+ const sourceNodeGrandParent = this._tree.findParentNode(
415
+ sourceNodeParent.id
416
+ );
417
+ if (sourceNodeGrandParent === null) {
418
+ this._tree.root = sourceNodeSibling;
419
+ } else if (sourceNodeGrandParent.right.id === sourceNodeParent.id) {
420
+ sourceNodeGrandParent.right = sourceNodeSibling;
421
+ } else if (sourceNodeGrandParent.left.id === sourceNodeParent.id) {
422
+ sourceNodeGrandParent.left = sourceNodeSibling;
423
+ }
424
+ const targetNodeParent = this._tree.findParentNode(targetId);
425
+ invariant(targetNodeParent !== null);
426
+ const splitNode = this.createSplitNode({
427
+ direction,
428
+ sourceNode,
429
+ targetNode
430
+ });
431
+ if (targetNodeParent.left.id === targetId) {
432
+ targetNodeParent.left = splitNode;
433
+ } else if (targetNodeParent.right.id === targetId) {
434
+ targetNodeParent.right = splitNode;
435
+ } else {
436
+ invariant(false);
437
+ }
438
+ this.syncLayoutRects();
439
+ }
440
+ resizePanel(id, point) {
441
+ if (this._tree.root === null) {
442
+ throw new Error("Root node is null");
443
+ }
444
+ const resizingRect = this.findRect(id);
445
+ if (resizingRect === null) {
446
+ throw new Error(`Rect with id ${id} not found`);
447
+ }
448
+ if (resizingRect.type !== "split") {
449
+ throw new Error(`Rect with id ${id} is not a split node`);
450
+ }
451
+ if (resizingRect.orientation === "horizontal") {
452
+ const { left: leftRect, right: rightRect } = this.getSplitChildRects(
453
+ resizingRect.id
454
+ );
455
+ const leftWidth = point.x - leftRect.x;
456
+ const totalWidth = leftRect.width + resizingRect.width + rightRect.width;
457
+ const ratio = clamp(leftWidth / totalWidth, 0.1, 0.9);
458
+ this.setSplitRatio(resizingRect.id, ratio);
459
+ } else if (resizingRect.orientation === "vertical") {
460
+ const { left: topRect, right: bottomRect } = this.getSplitChildRects(
461
+ resizingRect.id
462
+ );
463
+ const topHeight = point.y - topRect.y;
464
+ const totalHeight = topRect.height + resizingRect.height + bottomRect.height;
465
+ const ratio = clamp(topHeight / totalHeight, 0.1, 0.9);
466
+ this.setSplitRatio(resizingRect.id, ratio);
467
+ } else {
468
+ assertNever(resizingRect.orientation);
469
+ }
470
+ }
471
+ /**
472
+ /**
473
+ * The addPanelStrategy is used to determine panel placement when the `targetId` is `undefined`.
474
+ */
475
+ addPanel(options) {
476
+ const id = options?.id ?? crypto.randomUUID();
477
+ if (this._tree.root === null) {
478
+ this._tree.root = {
479
+ id,
480
+ type: "panel"
481
+ };
482
+ this.syncLayoutRects();
483
+ return;
484
+ }
485
+ const shouldUseStrategy = options?.targetId === void 0;
486
+ const {
487
+ targetId,
488
+ direction = "right",
489
+ ratio = 0.5
490
+ } = shouldUseStrategy ? this._addPanelStrategy.getPlacement(this._tree.root) : options;
491
+ invariant(targetId !== void 0, "targetId is not undefined");
492
+ if (targetId === this._tree.root.id) {
493
+ this._tree.root = this.createSplitNode({
494
+ direction,
495
+ ratio,
496
+ sourceNode: {
497
+ id,
498
+ type: "panel"
499
+ },
500
+ targetNode: this._tree.root
501
+ });
502
+ this.syncLayoutRects();
503
+ return;
504
+ }
505
+ const targetNode = this._tree.findNode(targetId);
506
+ if (targetNode === null) {
507
+ throw new Error(`Node with id ${targetId} not found`);
508
+ }
509
+ const targetNodeParent = this._tree.findParentNode(targetId);
510
+ invariant(targetNodeParent !== null, "Target node parent is not null");
511
+ const splitNode = this.createSplitNode({
512
+ direction,
513
+ sourceNode: {
514
+ id,
515
+ type: "panel"
516
+ },
517
+ targetNode,
518
+ ratio
519
+ });
520
+ this._tree.replaceChildNode({
521
+ parentId: targetNodeParent.id,
522
+ oldChildId: targetId,
523
+ newChild: splitNode
524
+ });
525
+ this.syncLayoutRects();
526
+ }
527
+ calculateDropTarget({
528
+ draggedPanelId,
529
+ targetPanelId,
530
+ point
531
+ }) {
532
+ invariant(
533
+ draggedPanelId !== targetPanelId,
534
+ "Dragged panel id is not the same as target panel id"
535
+ );
536
+ const targetRect = this.findRect(targetPanelId);
537
+ invariant(targetRect !== null && targetRect.type === "panel");
538
+ return {
539
+ id: targetPanelId,
540
+ direction: findClosestDirection(targetRect, point)
541
+ };
542
+ }
543
+ serialize() {
544
+ return JSON.stringify({
545
+ root: this._tree.root,
546
+ options: {
547
+ gap: this._options.gap
548
+ }
549
+ });
550
+ }
551
+ emit() {
552
+ this._eventEmitter.emit();
553
+ }
554
+ syncLayoutRects() {
555
+ this._layoutRects = calculateLayoutRects(this._tree.root, this._options);
556
+ this.emit();
557
+ }
558
+ createSplitNode({
559
+ direction,
560
+ sourceNode,
561
+ targetNode,
562
+ ratio = 0.5
563
+ }) {
564
+ switch (direction) {
565
+ case "left": {
566
+ return {
567
+ id: crypto.randomUUID(),
568
+ type: "split",
569
+ orientation: "horizontal",
570
+ ratio,
571
+ left: sourceNode,
572
+ right: targetNode
573
+ };
574
+ }
575
+ case "right": {
576
+ return {
577
+ id: crypto.randomUUID(),
578
+ type: "split",
579
+ orientation: "horizontal",
580
+ ratio,
581
+ left: targetNode,
582
+ right: sourceNode
583
+ };
584
+ }
585
+ case "top": {
586
+ return {
587
+ id: crypto.randomUUID(),
588
+ type: "split",
589
+ orientation: "vertical",
590
+ ratio,
591
+ left: sourceNode,
592
+ right: targetNode
593
+ };
594
+ }
595
+ case "bottom": {
596
+ return {
597
+ id: crypto.randomUUID(),
598
+ type: "split",
599
+ orientation: "vertical",
600
+ ratio,
601
+ left: targetNode,
602
+ right: sourceNode
603
+ };
604
+ }
605
+ default: {
606
+ assertNever(direction);
607
+ }
608
+ }
609
+ }
610
+ setSplitRatio(id, ratio) {
611
+ const splitNode = this._tree.findNode(id);
612
+ invariant(splitNode !== null, "Node is not null");
613
+ invariant(splitNode.type === "split", "Node is a split");
614
+ if (splitNode.orientation === "horizontal") {
615
+ const totalWidth = this.getSurroundingRect(splitNode.left.id).width + this._options.gap + this.getSurroundingRect(splitNode.right.id).width;
616
+ const minLeftWidth = calculateMinSize(
617
+ splitNode.left,
618
+ this._options.gap
619
+ ).width;
620
+ const minRightWidth = calculateMinSize(
621
+ splitNode.right,
622
+ this._options.gap
623
+ ).width;
624
+ const minRatio = (minLeftWidth + this._options.gap / 2) / totalWidth;
625
+ const maxRatio = (totalWidth - (minRightWidth + this._options.gap / 2)) / totalWidth;
626
+ splitNode.ratio = clamp(ratio, minRatio, maxRatio);
627
+ } else if (splitNode.orientation === "vertical") {
628
+ const totalHeight = this.getSurroundingRect(splitNode.left.id).height + this._options.gap + this.getSurroundingRect(splitNode.right.id).height;
629
+ const minTopHeight = calculateMinSize(
630
+ splitNode.left,
631
+ this._options.gap
632
+ ).height;
633
+ const minBottomHeight = calculateMinSize(
634
+ splitNode.right,
635
+ this._options.gap
636
+ ).height;
637
+ const minRatio = (minTopHeight + this._options.gap / 2) / totalHeight;
638
+ const maxRatio = (totalHeight - (minBottomHeight + this._options.gap / 2)) / totalHeight;
639
+ splitNode.ratio = clamp(ratio, minRatio, maxRatio);
640
+ } else {
641
+ assertNever(splitNode.orientation);
642
+ }
643
+ this.syncLayoutRects();
644
+ }
645
+ findRect(id) {
646
+ return this._layoutRects.find((rect) => rect.id === id) ?? null;
647
+ }
648
+ getSurroundingRect(id) {
649
+ const node = this._tree.findNode(id);
650
+ invariant(node !== null, "Node is not null");
651
+ if (node.type === "panel") {
652
+ const rect = this.findRect(id);
653
+ invariant(rect !== null, "Rect is not null");
654
+ invariant(rect.type === "panel", "Rect is a panel");
655
+ return {
656
+ x: rect.x,
657
+ y: rect.y,
658
+ width: rect.width,
659
+ height: rect.height
660
+ };
661
+ }
662
+ const leftRect = this.getSurroundingRect(node.left.id);
663
+ const rightRect = this.getSurroundingRect(node.right.id);
664
+ if (node.orientation === "horizontal") {
665
+ return {
666
+ x: leftRect.x,
667
+ y: leftRect.y,
668
+ width: leftRect.width + this._options.gap + rightRect.width,
669
+ height: leftRect.height
670
+ };
671
+ } else if (node.orientation === "vertical") {
672
+ return {
673
+ x: leftRect.x,
674
+ y: leftRect.y,
675
+ width: leftRect.width,
676
+ height: leftRect.height + this._options.gap + rightRect.height
677
+ };
678
+ } else {
679
+ assertNever(node.orientation);
680
+ }
681
+ }
682
+ getSplitChildRects(splitId) {
683
+ const node = this._tree.findNode(splitId);
684
+ invariant(node !== null && node.type === "split");
685
+ return {
686
+ left: this.getSurroundingRect(node.left.id),
687
+ right: this.getSurroundingRect(node.right.id)
688
+ };
689
+ }
690
+ };
691
+
692
+ // src/useDockLayout.ts
693
+ function useDockLayout(initialRoot, options) {
694
+ const [layoutManager] = useState(() => {
695
+ return new LayoutManager(initialRoot, options);
696
+ });
697
+ const containerRef = useRef(null);
698
+ const layoutRects = useSyncExternalStore(
699
+ layoutManager.subscribe,
700
+ () => layoutManager.layoutRects
701
+ );
702
+ const [resizingRect, setResizingRect] = useState(
703
+ null
704
+ );
705
+ const [draggingRect, setDraggingRect] = useState(
706
+ null
707
+ );
708
+ const [dropTarget, setDropTarget] = useState(null);
709
+ useLayoutEffect(() => {
710
+ const container = containerRef.current;
711
+ invariant(container !== null);
712
+ layoutManager.setSize({
713
+ width: container.clientWidth,
714
+ height: container.clientHeight
715
+ });
716
+ const resizeObserver = new ResizeObserver((entries) => {
717
+ for (const entry of entries) {
718
+ layoutManager.setSize({
719
+ width: entry.contentRect.width,
720
+ height: entry.contentRect.height
721
+ });
722
+ }
723
+ });
724
+ resizeObserver.observe(container);
725
+ return () => {
726
+ resizeObserver.disconnect();
727
+ };
728
+ }, [layoutManager]);
729
+ useEffect(() => {
730
+ if (resizingRect === null) {
731
+ return;
732
+ }
733
+ function handleMouseMove(event) {
734
+ invariant(resizingRect !== null);
735
+ layoutManager.resizePanel(resizingRect.id, {
736
+ x: event.clientX,
737
+ y: event.clientY
738
+ });
739
+ }
740
+ function handleMouseUp() {
741
+ setResizingRect(null);
742
+ }
743
+ document.addEventListener("mousemove", handleMouseMove);
744
+ document.addEventListener("mouseup", handleMouseUp);
745
+ return () => {
746
+ document.removeEventListener("mousemove", handleMouseMove);
747
+ document.removeEventListener("mouseup", handleMouseUp);
748
+ };
749
+ }, [resizingRect, layoutManager]);
750
+ useEffect(() => {
751
+ document.body.style.cursor = resizingRect === null ? "default" : CURSORS[resizingRect.orientation];
752
+ }, [resizingRect]);
753
+ return {
754
+ containerRef,
755
+ layoutRects,
756
+ layoutManager,
757
+ getRectProps: (rect) => {
758
+ if (rect.type === "split") {
759
+ return {
760
+ style: {
761
+ position: "absolute",
762
+ left: rect.x,
763
+ top: rect.y,
764
+ width: rect.width,
765
+ height: rect.height,
766
+ cursor: CURSORS[rect.orientation]
767
+ },
768
+ onMouseDown: () => {
769
+ setResizingRect(rect);
770
+ },
771
+ onMouseUp: () => {
772
+ setResizingRect(null);
773
+ }
774
+ };
775
+ } else if (rect.type === "panel") {
776
+ return {
777
+ style: {
778
+ position: "absolute",
779
+ left: rect.x,
780
+ top: rect.y,
781
+ width: rect.width,
782
+ height: rect.height
783
+ },
784
+ onMouseMove: (event) => {
785
+ if (draggingRect === null) {
786
+ return;
787
+ }
788
+ if (draggingRect.id === rect.id) {
789
+ setDropTarget(null);
790
+ return;
791
+ }
792
+ const dropTarget2 = layoutManager.calculateDropTarget({
793
+ draggedPanelId: draggingRect.id,
794
+ targetPanelId: rect.id,
795
+ point: {
796
+ x: event.clientX,
797
+ y: event.clientY
798
+ }
799
+ });
800
+ setDropTarget(dropTarget2);
801
+ },
802
+ onMouseUp: (event) => {
803
+ if (draggingRect === null) {
804
+ return;
805
+ }
806
+ if (draggingRect.id === rect.id) {
807
+ setDraggingRect(null);
808
+ setDropTarget(null);
809
+ return;
810
+ }
811
+ layoutManager.movePanel({
812
+ sourceId: draggingRect.id,
813
+ targetId: rect.id,
814
+ point: {
815
+ x: event.clientX,
816
+ y: event.clientY
817
+ }
818
+ });
819
+ setDraggingRect(null);
820
+ setDropTarget(null);
821
+ }
822
+ };
823
+ } else {
824
+ assertNever(rect);
825
+ }
826
+ },
827
+ getDropZoneProps: (rect) => {
828
+ if (draggingRect === null) {
829
+ return null;
830
+ }
831
+ const isDropTargetRect = rect.id === dropTarget?.id;
832
+ if (!isDropTargetRect) {
833
+ return null;
834
+ }
835
+ return {
836
+ style: getDropZoneStyle(dropTarget.direction)
837
+ };
838
+ },
839
+ getDragHandleProps: (rect) => {
840
+ return {
841
+ onMouseDown: () => {
842
+ setDraggingRect(rect);
843
+ }
844
+ };
845
+ },
846
+ draggingRect
847
+ };
848
+ }
849
+ function getDropZoneStyle(direction) {
850
+ if (direction === "top") {
851
+ return {
852
+ position: "absolute",
853
+ left: 0,
854
+ top: 0,
855
+ width: "100%",
856
+ height: "50%"
857
+ };
858
+ } else if (direction === "bottom") {
859
+ return {
860
+ position: "absolute",
861
+ left: 0,
862
+ top: "50%",
863
+ width: "100%",
864
+ height: "50%"
865
+ };
866
+ } else if (direction === "left") {
867
+ return {
868
+ position: "absolute",
869
+ left: 0,
870
+ top: 0,
871
+ width: "50%",
872
+ height: "100%"
873
+ };
874
+ } else if (direction === "right") {
875
+ return {
876
+ position: "absolute",
877
+ left: "50%",
878
+ top: 0,
879
+ width: "50%",
880
+ height: "100%"
881
+ };
882
+ } else {
883
+ assertNever(direction);
884
+ }
885
+ }
886
+ var CURSORS = {
887
+ horizontal: "col-resize",
888
+ vertical: "row-resize"
889
+ };
890
+ export {
891
+ bspStrategy,
892
+ evenlyDividedHorizontalStrategy,
893
+ useDockLayout
894
+ };