refactor(layouts): simplify to global config + preserve windows + add UI dropdown

Simplified layout system based on user feedback:

**1. Global Layout Config (Simpler)**
- Moved layoutConfig from per-workspace to global state
- One configuration applies to all workspaces (easier to understand)
- Updated state migration v8→v9 to move config to global level
- Updated WorkspaceSettings UI to edit global config
- Renamed updateWorkspaceLayoutConfig → updateLayoutConfig

**2. Preserve Extra Windows (Fixed Bug)**
- Fixed applyPresetToLayout to keep windows beyond preset slots
- When applying 4-window grid to 6 windows, windows 5-6 are preserved
- Extra windows stacked vertically on right side (70/30 split)
- No more window loss when applying presets

**3. Layout Dropdown in TabBar (Better UX)**
- Added dropdown menu next to workspace tabs
- Shows all available presets with icons (Grid2X2, Columns2, Split)
- Displays window requirements and availability
- Disables presets that need more windows than available
- One-click preset application with toast feedback
- More accessible than /layout command

All tests passing (457 passed). State migration handles v6→v7→v8→v9 correctly.

Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Alejandro Gómez
2025-12-18 12:24:00 +01:00
parent 52f39a8073
commit 4db7e9690c
9 changed files with 277 additions and 115 deletions

View File

@@ -131,16 +131,16 @@ describe("findLowestAvailableWorkspaceNumber", () => {
describe("addWindow", () => {
// Helper to create minimal test state
const createTestState = (layoutConfig: LayoutConfig, existingLayout: MosaicNode<string> | null = null): GrimoireState => ({
__version: 8,
__version: 9,
windows: {},
activeWorkspaceId: "test-workspace",
layoutConfig, // Global layout config (not per-workspace)
workspaces: {
"test-workspace": {
id: "test-workspace",
number: 1,
windowIds: [],
layout: existingLayout,
layoutConfig,
},
},
});

View File

@@ -76,8 +76,8 @@ export const addWindow = (
commandString: payload.commandString,
};
// Insert window using workspace layout configuration
const newLayout = insertWindow(ws.layout, newWindowId, ws.layoutConfig);
// Insert window using global layout configuration
const newLayout = insertWindow(ws.layout, newWindowId, state.layoutConfig);
return {
...state,
@@ -347,30 +347,18 @@ export const updateWindow = (
};
/**
* Updates the layout configuration for a workspace.
* Controls how new windows are inserted into the workspace layout.
* Updates the global layout configuration.
* Controls how new windows are inserted into all workspaces.
*/
export const updateWorkspaceLayoutConfig = (
export const updateLayoutConfig = (
state: GrimoireState,
workspaceId: string,
layoutConfig: Partial<GrimoireState["workspaces"][string]["layoutConfig"]>,
layoutConfig: Partial<GrimoireState["layoutConfig"]>,
): GrimoireState => {
const workspace = state.workspaces[workspaceId];
if (!workspace) {
return state; // Workspace doesn't exist, return unchanged
}
return {
...state,
workspaces: {
...state.workspaces,
[workspaceId]: {
...workspace,
layoutConfig: {
...workspace.layoutConfig,
...layoutConfig,
},
},
layoutConfig: {
...state.layoutConfig,
...layoutConfig,
},
};
};

View File

@@ -18,14 +18,14 @@ const initialState: GrimoireState = {
number: 1,
windowIds: [],
layout: null,
layoutConfig: {
insertionMode: "smart", // Smart auto-balancing by default
splitPercentage: 50, // Equal split
insertionPosition: "second", // New windows on right/bottom
autoPreset: undefined, // No preset maintenance
},
},
},
layoutConfig: {
insertionMode: "smart", // Smart auto-balancing by default
splitPercentage: 50, // Equal split
insertionPosition: "second", // New windows on right/bottom
autoPreset: undefined, // No preset maintenance
},
};
// Custom storage with error handling and migrations
@@ -229,14 +229,9 @@ export const useGrimoire = () => {
[setState],
);
const updateWorkspaceLayoutConfig = useCallback(
(
workspaceId: string,
layoutConfig: Partial<GrimoireState["workspaces"][string]["layoutConfig"]>,
) =>
setState((prev) =>
Logic.updateWorkspaceLayoutConfig(prev, workspaceId, layoutConfig),
),
const updateLayoutConfig = useCallback(
(layoutConfig: Partial<GrimoireState["layoutConfig"]>) =>
setState((prev) => Logic.updateLayoutConfig(prev, layoutConfig)),
[setState],
);
@@ -259,7 +254,7 @@ export const useGrimoire = () => {
setActiveWorkspace,
setActiveAccount,
setActiveAccountRelays,
updateWorkspaceLayoutConfig,
updateLayoutConfig,
applyPresetLayout,
};
};