feat: balance layout and kbd workspace navigation

This commit is contained in:
Alejandro Gómez
2025-12-18 13:47:40 +01:00
parent 1581e313f3
commit b57ff31907
7 changed files with 265 additions and 3 deletions

View File

@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
collectWindowIds,
applyPresetToLayout,
balanceLayout,
BUILT_IN_PRESETS,
} from "./layout-presets";
import type { MosaicNode } from "react-mosaic-component";
@@ -193,6 +194,84 @@ describe("layout-presets", () => {
});
});
describe("balanceLayout", () => {
it("returns null for null layout", () => {
expect(balanceLayout(null)).toBeNull();
});
it("returns single window unchanged", () => {
expect(balanceLayout("w1")).toBe("w1");
});
it("balances a simple binary split", () => {
const unbalanced: MosaicNode<string> = {
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 70,
};
const balanced = balanceLayout(unbalanced);
expect(balanced).toEqual({
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 50,
});
});
it("balances nested splits recursively", () => {
const unbalanced: MosaicNode<string> = {
direction: "row",
first: {
direction: "column",
first: "w1",
second: "w2",
splitPercentage: 30,
},
second: {
direction: "column",
first: "w3",
second: "w4",
splitPercentage: 80,
},
splitPercentage: 60,
};
const balanced = balanceLayout(unbalanced);
// All splits should be 50%
expect(balanced).toMatchObject({
splitPercentage: 50,
first: { splitPercentage: 50 },
second: { splitPercentage: 50 },
});
});
it("preserves window IDs and directions", () => {
const original: MosaicNode<string> = {
direction: "column",
first: "w1",
second: {
direction: "row",
first: "w2",
second: "w3",
splitPercentage: 75,
},
splitPercentage: 25,
};
const balanced = balanceLayout(original);
const windowIds = collectWindowIds(balanced);
expect(windowIds).toEqual(["w1", "w2", "w3"]);
// Check directions preserved
if (balanced && typeof balanced !== "string") {
expect(balanced.direction).toBe("column");
if (typeof balanced.second !== "string") {
expect(balanced.second.direction).toBe("row");
}
}
});
});
describe("applyPresetToLayout", () => {
it("throws error if too few windows", () => {
const layout: MosaicNode<string> = "w1";

View File

@@ -203,6 +203,31 @@ export function applyPresetToLayout(
return preset.generate(windowIds);
}
/**
* Balances all split percentages in a layout tree to 50/50
* Useful for equalizing splits after manual resizing
*/
export function balanceLayout(
layout: MosaicNode<string> | null
): MosaicNode<string> | null {
if (layout === null) {
return null;
}
// Leaf node (window ID), return as-is
if (typeof layout === "string") {
return layout;
}
// Branch node, balance this split and recurse
return {
direction: layout.direction,
first: balanceLayout(layout.first),
second: balanceLayout(layout.second),
splitPercentage: 50,
};
}
/**
* Get a preset by ID
*/