react-headless-dock-layout 0.1.0 → 0.1.2

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.
@@ -1,77 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { PanelNode, SplitNode } from "../../types";
3
- import { calculateMinSize } from "./calculateMinSize";
4
-
5
- describe("calculateMinSize", () => {
6
- it("should return the min size of a panel node", () => {
7
- const node: PanelNode = {
8
- type: "panel",
9
- id: "panel",
10
- minSize: { width: 100, height: 100 },
11
- };
12
- const result = calculateMinSize(node, 10);
13
- expect(result).toEqual({ width: 100, height: 100 });
14
- });
15
-
16
- it("should return the min size of a split node when the orientation is horizontal", () => {
17
- const node: SplitNode = {
18
- type: "split",
19
- id: "split",
20
- orientation: "horizontal",
21
- ratio: 0.5,
22
- left: { type: "panel", id: "left", minSize: { width: 100, height: 100 } },
23
- right: {
24
- type: "panel",
25
- id: "right",
26
- minSize: { width: 100, height: 100 },
27
- },
28
- };
29
- const result = calculateMinSize(node, 10);
30
- expect(result).toEqual({ width: 210, height: 100 });
31
- });
32
-
33
- it("should return the min size of a split node when the orientation is vertical", () => {
34
- const node: SplitNode = {
35
- type: "split",
36
- id: "split",
37
- orientation: "vertical",
38
- ratio: 0.5,
39
- left: { type: "panel", id: "left", minSize: { width: 100, height: 100 } },
40
- right: {
41
- type: "panel",
42
- id: "right",
43
- minSize: { width: 100, height: 100 },
44
- },
45
- };
46
- const result = calculateMinSize(node, 10);
47
- expect(result).toEqual({ width: 100, height: 210 });
48
- });
49
-
50
- it("should return the min size of a split node when the node is nested", () => {
51
- const node: SplitNode = {
52
- type: "split",
53
- id: "split",
54
- orientation: "horizontal",
55
- ratio: 0.5,
56
- left: { type: "panel", id: "left", minSize: { width: 100, height: 100 } },
57
- right: {
58
- type: "split",
59
- id: "right",
60
- orientation: "vertical",
61
- ratio: 0.5,
62
- left: {
63
- type: "panel",
64
- id: "right-left",
65
- minSize: { width: 100, height: 100 },
66
- },
67
- right: {
68
- type: "panel",
69
- id: "right-right",
70
- minSize: { width: 100, height: 100 },
71
- },
72
- },
73
- };
74
- const result = calculateMinSize(node, 10);
75
- expect(result).toEqual({ width: 210, height: 210 });
76
- });
77
- });
@@ -1,40 +0,0 @@
1
- import type { LayoutNode } from "../../types";
2
- import { assertNever } from "../assertNever";
3
- import type { Size } from "./types";
4
-
5
- export function calculateMinSize(node: LayoutNode, gap: number): Size {
6
- if (node.type === "panel") {
7
- return {
8
- width: node.minSize?.width ?? 0,
9
- height: node.minSize?.height ?? 0,
10
- };
11
- } else if (node.type === "split") {
12
- if (node.orientation === "horizontal") {
13
- return {
14
- width:
15
- calculateMinSize(node.left, gap).width +
16
- gap +
17
- calculateMinSize(node.right, gap).width,
18
- height: Math.max(
19
- calculateMinSize(node.left, gap).height,
20
- calculateMinSize(node.right, gap).height,
21
- ),
22
- };
23
- } else if (node.orientation === "vertical") {
24
- return {
25
- width: Math.max(
26
- calculateMinSize(node.left, gap).width,
27
- calculateMinSize(node.right, gap).width,
28
- ),
29
- height:
30
- calculateMinSize(node.left, gap).height +
31
- gap +
32
- calculateMinSize(node.right, gap).height,
33
- };
34
- } else {
35
- assertNever(node.orientation);
36
- }
37
- } else {
38
- assertNever(node);
39
- }
40
- }
@@ -1,95 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { findClosestDirection } from "./findClosestDirection";
3
-
4
- describe("findClosestDirection", () => {
5
- it("should return top when the point is at the top edge", () => {
6
- const rect = { x: 0, y: 0, width: 100, height: 100 };
7
- const point = { x: 50, y: 0 };
8
- const result = findClosestDirection(rect, point);
9
- expect(result).toBe("top");
10
- });
11
-
12
- it("should return bottom when the point is at the bottom edge", () => {
13
- const rect = { x: 0, y: 0, width: 100, height: 100 };
14
- const point = { x: 50, y: 100 };
15
- const result = findClosestDirection(rect, point);
16
- expect(result).toBe("bottom");
17
- });
18
-
19
- it("should return left when the point is at the left edge", () => {
20
- const rect = { x: 0, y: 0, width: 100, height: 100 };
21
- const point = { x: 0, y: 50 };
22
- const result = findClosestDirection(rect, point);
23
- expect(result).toBe("left");
24
- });
25
-
26
- it("should return right when the point is at the right edge", () => {
27
- const rect = { x: 0, y: 0, width: 100, height: 100 };
28
- const point = { x: 100, y: 50 };
29
- const result = findClosestDirection(rect, point);
30
- expect(result).toBe("right");
31
- });
32
-
33
- it("should return top when the point is in the top directional region", () => {
34
- const rect = { x: 0, y: 0, width: 100, height: 100 };
35
- const point = { x: 50, y: 10 };
36
- const result = findClosestDirection(rect, point);
37
- expect(result).toBe("top");
38
- });
39
-
40
- it("should return bottom when the point is in the bottom directional region", () => {
41
- const rect = { x: 0, y: 0, width: 100, height: 100 };
42
- const point = { x: 50, y: 90 };
43
- const result = findClosestDirection(rect, point);
44
- expect(result).toBe("bottom");
45
- });
46
-
47
- it("should return left when the point is in the left directional region", () => {
48
- const rect = { x: 0, y: 0, width: 100, height: 100 };
49
- const point = { x: 10, y: 50 };
50
- const result = findClosestDirection(rect, point);
51
- expect(result).toBe("left");
52
- });
53
-
54
- it("should return right when the point is in the right directional region", () => {
55
- const rect = { x: 0, y: 0, width: 100, height: 100 };
56
- const point = { x: 90, y: 50 };
57
- const result = findClosestDirection(rect, point);
58
- expect(result).toBe("right");
59
- });
60
-
61
- it("returns top when the point lies on the boundary between left and top directional regions", () => {
62
- const rect = { x: 0, y: 0, width: 100, height: 100 };
63
- const point = { x: 25, y: 25 };
64
- const result = findClosestDirection(rect, point);
65
- expect(result).toBe("top");
66
- });
67
-
68
- it("returns top when the point lies on the boundary between right and top directional regions", () => {
69
- const rect = { x: 0, y: 0, width: 100, height: 100 };
70
- const point = { x: 75, y: 25 };
71
- const result = findClosestDirection(rect, point);
72
- expect(result).toBe("top");
73
- });
74
-
75
- it("should return bottom when the point lies on the boundary between left and bottom directional regions", () => {
76
- const rect = { x: 0, y: 0, width: 100, height: 100 };
77
- const point = { x: 25, y: 75 };
78
- const result = findClosestDirection(rect, point);
79
- expect(result).toBe("bottom");
80
- });
81
-
82
- it("should return bottom when the point lies on the boundary between right and bottom directional regions", () => {
83
- const rect = { x: 0, y: 0, width: 100, height: 100 };
84
- const point = { x: 75, y: 75 };
85
- const result = findClosestDirection(rect, point);
86
- expect(result).toBe("bottom");
87
- });
88
-
89
- it("should return top when the point lies on the center of the rect", () => {
90
- const rect = { x: 0, y: 0, width: 100, height: 100 };
91
- const point = { x: 50, y: 50 };
92
- const result = findClosestDirection(rect, point);
93
- expect(result).toBe("top");
94
- });
95
- });
@@ -1,15 +0,0 @@
1
- import type { Direction, Point, Rect } from "./types";
2
-
3
- export function findClosestDirection(rect: Rect, point: Point): Direction {
4
- const centerX = rect.x + rect.width / 2;
5
- const centerY = rect.y + rect.height / 2;
6
-
7
- const dx = (point.x - centerX) / (rect.width / 2);
8
- const dy = (point.y - centerY) / (rect.height / 2);
9
-
10
- if (Math.abs(dx) > Math.abs(dy)) {
11
- return dx > 0 ? "right" : "left";
12
- } else {
13
- return dy > 0 ? "bottom" : "top";
14
- }
15
- }
@@ -1,20 +0,0 @@
1
- export interface Size {
2
- width: number;
3
- height: number;
4
- }
5
-
6
- export interface Point {
7
- x: number;
8
- y: number;
9
- }
10
-
11
- export interface Rect {
12
- x: number;
13
- y: number;
14
- width: number;
15
- height: number;
16
- }
17
-
18
- export type Orientation = "horizontal" | "vertical";
19
-
20
- export type Direction = "top" | "bottom" | "left" | "right";
@@ -1,3 +0,0 @@
1
- export function assertNever(value: never): never {
2
- throw new Error(`Unexpected value: ${value}`);
3
- }
@@ -1,3 +0,0 @@
1
- export function clamp(value: number, min: number, max: number) {
2
- return Math.max(min, Math.min(max, value));
3
- }
@@ -1,30 +0,0 @@
1
- import type { LayoutNode, SplitNode } from "../types";
2
- import { assertNever } from "./assertNever";
3
-
4
- export function findParentNode(root: LayoutNode | null, id: string) {
5
- if (root === null) {
6
- return null;
7
- }
8
-
9
- return findParentNodeInSubTree(id, root);
10
- }
11
-
12
- function findParentNodeInSubTree(
13
- id: string,
14
- node: LayoutNode,
15
- ): SplitNode | null {
16
- if (node.type === "panel") {
17
- return null;
18
- } else if (node.type === "split") {
19
- if (node.left.id === id || node.right.id === id) {
20
- return node;
21
- }
22
-
23
- return (
24
- findParentNodeInSubTree(id, node.left) ??
25
- findParentNodeInSubTree(id, node.right)
26
- );
27
- } else {
28
- assertNever(node);
29
- }
30
- }
@@ -1,6 +0,0 @@
1
- // biome-ignore lint/suspicious/noExplicitAny: for flexibility condition can be any type
2
- export function invariant(condition: any, message?: string): asserts condition {
3
- if (!condition) {
4
- throw new Error(message ?? "Invariant failed");
5
- }
6
- }
package/src/strategies.ts DELETED
@@ -1,76 +0,0 @@
1
- import { assertNever } from "./internal/assertNever";
2
- import { findParentNode } from "./internal/findParentNode";
3
- import type { Direction } from "./internal/LayoutManager/types";
4
- import type { LayoutNode, PanelNode } from "./types";
5
-
6
- export interface AddPanelStrategy {
7
- getPlacement(root: LayoutNode): {
8
- targetId: string;
9
- direction: Direction;
10
- ratio: number;
11
- };
12
- }
13
-
14
- export const evenlyDividedHorizontalStrategy: AddPanelStrategy = {
15
- getPlacement(root) {
16
- const horizontalNodeCount = calculateHorizontalSplitCount(root) + 1;
17
-
18
- return {
19
- targetId: root.id,
20
- direction: "right",
21
- ratio: horizontalNodeCount / (horizontalNodeCount + 1),
22
- };
23
- },
24
- };
25
-
26
- function calculateHorizontalSplitCount(node: LayoutNode): number {
27
- if (node.type === "panel") {
28
- return 0;
29
- } else if (node.type === "split") {
30
- if (node.orientation === "horizontal") {
31
- return (
32
- 1 +
33
- calculateHorizontalSplitCount(node.left) +
34
- calculateHorizontalSplitCount(node.right)
35
- );
36
- } else if (node.orientation === "vertical") {
37
- return (
38
- 0 +
39
- calculateHorizontalSplitCount(node.left) +
40
- calculateHorizontalSplitCount(node.right)
41
- );
42
- } else {
43
- assertNever(node.orientation);
44
- }
45
- } else {
46
- assertNever(node);
47
- }
48
- }
49
-
50
- export const bspStrategy: AddPanelStrategy = {
51
- getPlacement(root) {
52
- const rightMostPanel = findRightMostPanel(root);
53
- const parentNode = findParentNode(root, rightMostPanel.id);
54
-
55
- return {
56
- targetId: rightMostPanel.id,
57
- direction:
58
- parentNode === null
59
- ? "right"
60
- : parentNode.orientation === "horizontal"
61
- ? "bottom"
62
- : "right",
63
- ratio: 0.5,
64
- };
65
- },
66
- };
67
-
68
- function findRightMostPanel(node: LayoutNode): PanelNode {
69
- if (node.type === "panel") {
70
- return node;
71
- } else if (node.type === "split") {
72
- return findRightMostPanel(node.right);
73
- } else {
74
- assertNever(node);
75
- }
76
- }
package/src/types.ts DELETED
@@ -1,31 +0,0 @@
1
- import type { Orientation, Rect, Size } from "./internal/LayoutManager/types";
2
-
3
- export interface LayoutManagerOptions {
4
- size?: Size;
5
- gap?: number;
6
- }
7
-
8
- export interface PanelLayoutRect extends Pick<PanelNode, "id" | "type">, Rect {}
9
-
10
- export interface SplitLayoutRect
11
- extends Pick<SplitNode, "id" | "type" | "orientation">,
12
- Rect {}
13
-
14
- export type LayoutRect = PanelLayoutRect | SplitLayoutRect;
15
-
16
- export interface PanelNode {
17
- type: "panel";
18
- id: string;
19
- minSize?: Partial<Size>;
20
- }
21
-
22
- export interface SplitNode {
23
- type: "split";
24
- id: string;
25
- left: LayoutNode;
26
- right: LayoutNode;
27
- orientation: Orientation;
28
- ratio: number;
29
- }
30
-
31
- export type LayoutNode = PanelNode | SplitNode;
@@ -1,249 +0,0 @@
1
- import {
2
- type CSSProperties,
3
- type MouseEvent as ReactMouseEvent,
4
- useEffect,
5
- useLayoutEffect,
6
- useRef,
7
- useState,
8
- useSyncExternalStore,
9
- } from "react";
10
- import { assertNever } from "./internal/assertNever";
11
- import { invariant } from "./internal/invariant";
12
- import { LayoutManager } from "./internal/LayoutManager/LayoutManager";
13
- import type { Direction } from "./internal/LayoutManager/types";
14
- import type {
15
- LayoutManagerOptions,
16
- LayoutNode,
17
- LayoutRect,
18
- PanelLayoutRect,
19
- SplitLayoutRect,
20
- } from "./types";
21
-
22
- export function useDockLayout<T extends HTMLElement>(
23
- initialRoot: LayoutNode | null,
24
- options?: LayoutManagerOptions,
25
- ) {
26
- const [layoutManager] = useState(() => {
27
- return new LayoutManager(initialRoot, options);
28
- });
29
- const containerRef = useRef<T>(null);
30
- const layoutRects = useSyncExternalStore(
31
- layoutManager.subscribe,
32
- () => layoutManager.layoutRects,
33
- );
34
- const [resizingRect, setResizingRect] = useState<SplitLayoutRect | null>(
35
- null,
36
- );
37
- const [draggingRect, setDraggingRect] = useState<PanelLayoutRect | null>(
38
- null,
39
- );
40
- const [dropTarget, setDropTarget] = useState<{
41
- id: string;
42
- direction: Direction;
43
- } | null>(null);
44
-
45
- useLayoutEffect(() => {
46
- const container = containerRef.current;
47
- invariant(container !== null);
48
-
49
- layoutManager.setSize({
50
- width: container.clientWidth,
51
- height: container.clientHeight,
52
- });
53
-
54
- const resizeObserver = new ResizeObserver((entries) => {
55
- for (const entry of entries) {
56
- layoutManager.setSize({
57
- width: entry.contentRect.width,
58
- height: entry.contentRect.height,
59
- });
60
- }
61
- });
62
-
63
- resizeObserver.observe(container);
64
-
65
- return () => {
66
- resizeObserver.disconnect();
67
- };
68
- }, [layoutManager]);
69
-
70
- useEffect(() => {
71
- if (resizingRect === null) {
72
- return;
73
- }
74
-
75
- function handleMouseMove(event: MouseEvent) {
76
- invariant(resizingRect !== null);
77
-
78
- layoutManager.resizePanel(resizingRect.id, {
79
- x: event.clientX,
80
- y: event.clientY,
81
- });
82
- }
83
-
84
- function handleMouseUp() {
85
- setResizingRect(null);
86
- }
87
-
88
- document.addEventListener("mousemove", handleMouseMove);
89
- document.addEventListener("mouseup", handleMouseUp);
90
-
91
- return () => {
92
- document.removeEventListener("mousemove", handleMouseMove);
93
- document.removeEventListener("mouseup", handleMouseUp);
94
- };
95
- }, [resizingRect, layoutManager]);
96
-
97
- useEffect(() => {
98
- document.body.style.cursor =
99
- resizingRect === null ? "default" : CURSORS[resizingRect.orientation];
100
- }, [resizingRect]);
101
-
102
- return {
103
- containerRef,
104
- layoutRects,
105
- layoutManager,
106
- getRectProps: (rect: LayoutRect) => {
107
- if (rect.type === "split") {
108
- return {
109
- style: {
110
- position: "absolute",
111
- left: rect.x,
112
- top: rect.y,
113
- width: rect.width,
114
- height: rect.height,
115
- cursor: CURSORS[rect.orientation],
116
- },
117
- onMouseDown: () => {
118
- setResizingRect(rect);
119
- },
120
- onMouseUp: () => {
121
- setResizingRect(null);
122
- },
123
- } as const;
124
- } else if (rect.type === "panel") {
125
- return {
126
- style: {
127
- position: "absolute",
128
- left: rect.x,
129
- top: rect.y,
130
- width: rect.width,
131
- height: rect.height,
132
- } as const,
133
- onMouseMove: (event: ReactMouseEvent<T>) => {
134
- if (draggingRect === null) {
135
- return;
136
- }
137
-
138
- if (draggingRect.id === rect.id) {
139
- setDropTarget(null);
140
- return;
141
- }
142
-
143
- const dropTarget = layoutManager.calculateDropTarget({
144
- draggedPanelId: draggingRect.id,
145
- targetPanelId: rect.id,
146
- point: {
147
- x: event.clientX,
148
- y: event.clientY,
149
- },
150
- });
151
- setDropTarget(dropTarget);
152
- },
153
- onMouseUp: (event: ReactMouseEvent<T>) => {
154
- if (draggingRect === null) {
155
- return;
156
- }
157
-
158
- if (draggingRect.id === rect.id) {
159
- setDraggingRect(null);
160
- setDropTarget(null);
161
- return;
162
- }
163
-
164
- layoutManager.movePanel({
165
- sourceId: draggingRect.id,
166
- targetId: rect.id,
167
- point: {
168
- x: event.clientX,
169
- y: event.clientY,
170
- },
171
- });
172
- setDraggingRect(null);
173
- setDropTarget(null);
174
- },
175
- } as const;
176
- } else {
177
- assertNever(rect);
178
- }
179
- },
180
- getDropZoneProps: (rect: PanelLayoutRect) => {
181
- if (draggingRect === null) {
182
- return null;
183
- }
184
-
185
- const isDropTargetRect = rect.id === dropTarget?.id;
186
- if (!isDropTargetRect) {
187
- return null;
188
- }
189
-
190
- return {
191
- style: getDropZoneStyle(dropTarget.direction),
192
- };
193
- },
194
- getDragHandleProps: (rect: PanelLayoutRect) => {
195
- return {
196
- onMouseDown: () => {
197
- setDraggingRect(rect);
198
- },
199
- };
200
- },
201
- draggingRect,
202
- };
203
- }
204
-
205
- function getDropZoneStyle(direction: Direction) {
206
- if (direction === "top") {
207
- return {
208
- position: "absolute",
209
- left: 0,
210
- top: 0,
211
- width: "100%",
212
- height: "50%",
213
- } as const;
214
- } else if (direction === "bottom") {
215
- return {
216
- position: "absolute",
217
- left: 0,
218
- top: "50%",
219
- width: "100%",
220
- height: "50%",
221
- } as const;
222
- } else if (direction === "left") {
223
- return {
224
- position: "absolute",
225
- left: 0,
226
- top: 0,
227
- width: "50%",
228
- height: "100%",
229
- } as const;
230
- } else if (direction === "right") {
231
- return {
232
- position: "absolute",
233
- left: "50%",
234
- top: 0,
235
- width: "50%",
236
- height: "100%",
237
- } as const;
238
- } else {
239
- assertNever(direction);
240
- }
241
- }
242
-
243
- const CURSORS: Record<
244
- SplitLayoutRect["orientation"],
245
- NonNullable<CSSProperties["cursor"]>
246
- > = {
247
- horizontal: "col-resize",
248
- vertical: "row-resize",
249
- };
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "@total-typescript/tsconfig/bundler/dom/library",
3
- "compilerOptions": {
4
- "jsx": "react-jsx"
5
- },
6
- "include": ["src"]
7
- }
package/tsup.config.ts DELETED
@@ -1,9 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.ts"],
5
- format: ["esm", "cjs"],
6
- splitting: true,
7
- clean: true,
8
- dts: true,
9
- });