Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export function App() {
getDropIndicatorProps,
getDragHandleProps,
} = useDockLayout<HTMLDivElement>(() => {
console.info("working");
const root = localStorage.getItem("layout");
if (root === null) {
return null;
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export * from "./strategies";
export * from "./types";
export * from "./useDockLayout";
export {
validateLayoutTree,
assertValidLayoutTree,
type ValidationError,
type ValidationResult,
} from "./internal/LayoutManager/validateLayoutTree";
24 changes: 12 additions & 12 deletions src/internal/LayoutManager/LayoutManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe("LayoutManager", () => {
const root = null;
const layoutManager = new LayoutManager(root);
expect(() => layoutManager.resizePanel("root", { x: 0, y: 0 })).toThrow(
"Root node is null",
/resizePanel.*Cannot resize.*empty layout/,
);
});

Expand All @@ -20,7 +20,7 @@ describe("LayoutManager", () => {
const layoutManager = new LayoutManager(root);
expect(() =>
layoutManager.resizePanel("non-existent-id", { x: 0, y: 0 }),
).toThrow("Rect with id non-existent-id not found");
).toThrow(/resizePanel.*non-existent-id.*not found/);
});

it("should throw an error when the rect is not a split node", () => {
Expand All @@ -30,7 +30,7 @@ describe("LayoutManager", () => {
};
const layoutManager = new LayoutManager(root);
expect(() => layoutManager.resizePanel("root", { x: 0, y: 0 })).toThrow(
"Rect with id root is not a split node",
/resizePanel.*Expected split.*got panel/,
);
});

Expand Down Expand Up @@ -190,7 +190,7 @@ describe("LayoutManager", () => {
it("should throw an error if the root is null", () => {
const layoutManager = new LayoutManager(null);
expect(() => layoutManager.removePanel("root")).toThrowError(
"Root node is null",
/removePanel.*Cannot remove.*empty layout/,
);
});

Expand All @@ -200,7 +200,7 @@ describe("LayoutManager", () => {
type: "panel",
});
expect(() => layoutManager.removePanel("nonexistent")).toThrowError(
"Node with id nonexistent not found",
/removePanel.*nonexistent.*not found/,
);
});

Expand All @@ -220,7 +220,7 @@ describe("LayoutManager", () => {
},
});
expect(() => layoutManager.removePanel("root")).toThrowError(
"Node with id root is not a panel",
/removePanel.*root.*is a split.*expected panel/,
);
});

Expand Down Expand Up @@ -312,7 +312,7 @@ describe("LayoutManager", () => {
targetId: "target",
point: { x: 0, y: 0 },
}),
).toThrowError("Root node is null");
).toThrowError(/movePanel.*Cannot move.*empty layout/);
});

it("should throw an error if the root is not a split node", () => {
Expand All @@ -326,7 +326,7 @@ describe("LayoutManager", () => {
targetId: "target",
point: { x: 0, y: 0 },
}),
).toThrowError("Root node is not a split node");
).toThrowError(/movePanel.*Cannot move.*only one panel/);
});

it("should throw an error if the source node is not found", () => {
Expand All @@ -350,7 +350,7 @@ describe("LayoutManager", () => {
targetId: "target",
point: { x: 0, y: 0 },
}),
).toThrowError("Node with id nonexistent not found");
).toThrowError(/movePanel.*Source panel "nonexistent" not found/);
});

it("should throw an error if the source node is not a panel node", () => {
Expand Down Expand Up @@ -384,7 +384,7 @@ describe("LayoutManager", () => {
targetId: "target",
point: { x: 0, y: 0 },
}),
).toThrowError("Node with id left is not a panel node");
).toThrowError(/movePanel.*Source "left" is a split.*expected panel/);
});

it("should throw an error if the target node is not found", () => {
Expand All @@ -408,7 +408,7 @@ describe("LayoutManager", () => {
targetId: "nonexistent",
point: { x: 0, y: 0 },
}),
).toThrowError("Node with id nonexistent not found");
).toThrowError(/movePanel.*Target panel "nonexistent" not found/);
});

it("should throw an error if the target node is not a panel node", () => {
Expand Down Expand Up @@ -442,7 +442,7 @@ describe("LayoutManager", () => {
targetId: "right",
point: { x: 0, y: 0 },
}),
).toThrowError("Node with id right is not a panel node");
).toThrowError(/movePanel.*Target "right" is a split.*expected panel/);
});

it("should move the panel when the source node is the sibling of the target node", () => {
Expand Down
58 changes: 45 additions & 13 deletions src/internal/LayoutManager/LayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { calculateMinSize } from "./calculateMinSize";
import { findClosestDirection } from "./findClosestDirection";
import { LayoutTree } from "./LayoutTree";
import type { Direction, Point, Rect, Size } from "./types";
import { assertValidLayoutTree } from "./validateLayoutTree";

export class LayoutManager {
private readonly MIN_RESIZE_RATIO = 0.1;
Expand All @@ -27,6 +28,9 @@ export class LayoutManager {
private _layoutRects: LayoutRect[] = [];

constructor(root: LayoutNode | null, options?: LayoutManagerOptions) {
// Validate the initial layout tree to catch errors early
assertValidLayoutTree(root);

this._tree = new LayoutTree(root);
this._options = {
gap: options?.gap ?? 10,
Expand All @@ -42,6 +46,8 @@ export class LayoutManager {
}

set root(root: LayoutNode | null) {
// Validate before setting to catch errors early
assertValidLayoutTree(root);
this._tree.root = root;
this.syncLayoutRects();
}
Expand All @@ -65,17 +71,23 @@ export class LayoutManager {

removePanel(id: string) {
if (this._tree.root === null) {
throw new Error("Root node is null");
throw new Error(
`removePanel("${id}"): Cannot remove panel from empty layout`,
);
}

const node = this._tree.findNode(id);

if (node === null) {
throw new Error(`Node with id ${id} not found`);
throw new Error(
`removePanel("${id}"): Panel not found in layout tree`,
);
}

if (node.type !== "panel") {
throw new Error(`Node with id ${id} is not a panel`);
throw new Error(
`removePanel("${id}"): Node is a ${node.type}, expected panel`,
);
}

if (node.id === this._tree.root.id) {
Expand Down Expand Up @@ -117,19 +129,27 @@ export class LayoutManager {
point: Point;
}) {
if (this._tree.root === null) {
throw new Error("Root node is null");
throw new Error(
`movePanel("${sourceId}" → "${targetId}"): Cannot move panel in empty layout`,
);
}
if (this._tree.root.type !== "split") {
throw new Error("Root node is not a split node");
throw new Error(
`movePanel("${sourceId}" → "${targetId}"): Cannot move panel when layout has only one panel`,
);
}

const sourceNode = this._tree.findNode(sourceId);

if (sourceNode === null) {
throw new Error(`Node with id ${sourceId} not found`);
throw new Error(
`movePanel("${sourceId}" → "${targetId}"): Source panel "${sourceId}" not found`,
);
}
if (sourceNode.type !== "panel") {
throw new Error(`Node with id ${sourceId} is not a panel node`);
throw new Error(
`movePanel("${sourceId}" → "${targetId}"): Source "${sourceId}" is a ${sourceNode.type}, expected panel`,
);
}

const sourceNodeParent = this._tree.findParentNode(sourceId);
Expand All @@ -138,10 +158,14 @@ export class LayoutManager {
const targetNode = this._tree.findNode(targetId);

if (targetNode === null) {
throw new Error(`Node with id ${targetId} not found`);
throw new Error(
`movePanel("${sourceId}" → "${targetId}"): Target panel "${targetId}" not found`,
);
}
if (targetNode.type !== "panel") {
throw new Error(`Node with id ${targetId} is not a panel node`);
throw new Error(
`movePanel("${sourceId}" → "${targetId}"): Target "${targetId}" is a ${targetNode.type}, expected panel`,
);
}

const sourceNodeSibling =
Expand Down Expand Up @@ -210,17 +234,23 @@ export class LayoutManager {

resizePanel(id: string, point: Point) {
if (this._tree.root === null) {
throw new Error("Root node is null");
throw new Error(
`resizePanel("${id}"): Cannot resize in empty layout`,
);
}

const resizingRect = this.findRect(id);

if (resizingRect === null) {
throw new Error(`Rect with id ${id} not found`);
throw new Error(
`resizePanel("${id}"): Split bar not found in layout`,
);
}

if (resizingRect.type !== "split") {
throw new Error(`Rect with id ${id} is not a split node`);
throw new Error(
`resizePanel("${id}"): Expected split bar, got ${resizingRect.type}`,
);
}

const splitNode = this._tree.findNode(id);
Expand Down Expand Up @@ -266,7 +296,9 @@ export class LayoutManager {
const targetNode = this._tree.findNode(targetId);

if (targetNode === null) {
throw new Error(`Node with id ${targetId} not found`);
throw new Error(
`addPanel("${id}"): Placement strategy returned target "${targetId}" which was not found in layout tree`,
);
}

const targetNodeParent = this._tree.findParentNode(targetId);
Expand Down
Loading