Files
grimoire/src/lib/layout-presets.ts
Alejandro Gómez a6650ff6e1 fix: type errors
2025-12-18 16:00:56 +01:00

243 lines
5.9 KiB
TypeScript

import type { MosaicNode } from "react-mosaic-component";
/**
* A layout preset that can be applied to arrange windows
*/
export interface LayoutPreset {
/** Unique identifier for the preset */
id: string;
/** Display name for the preset */
name: string;
/** Description of the layout arrangement */
description: string;
/** Minimum number of windows required */
minSlots: number;
/** Maximum number of windows (undefined = no limit) */
maxSlots?: number;
/** Function to generate layout for given window IDs */
generate: (windowIds: string[]) => MosaicNode<string>;
}
/**
* Builds a horizontal row of windows with equal splits
*/
function buildHorizontalRow(windowIds: string[]): MosaicNode<string> {
if (windowIds.length === 0) {
throw new Error("Cannot build row with zero windows");
}
if (windowIds.length === 1) {
return windowIds[0];
}
// Calculate percentage for first window to make equal splits
const splitPercent = 100 / windowIds.length;
return {
direction: "row",
first: windowIds[0],
second: buildHorizontalRow(windowIds.slice(1)),
splitPercentage: splitPercent,
};
}
/**
* Builds a vertical stack of windows with equal splits
*/
function buildVerticalStack(windowIds: string[]): MosaicNode<string> {
if (windowIds.length === 0) {
throw new Error("Cannot build stack with zero windows");
}
if (windowIds.length === 1) {
return windowIds[0];
}
// Calculate percentage for first window to make equal splits
const splitPercent = 100 / windowIds.length;
return {
direction: "column",
first: windowIds[0],
second: buildVerticalStack(windowIds.slice(1)),
splitPercentage: splitPercent,
};
}
/**
* Builds a vertical stack of MosaicNodes with equal splits
*/
function buildVerticalStackOfNodes(
nodes: MosaicNode<string>[],
): MosaicNode<string> {
if (nodes.length === 0) {
throw new Error("Cannot build stack with zero nodes");
}
if (nodes.length === 1) {
return nodes[0];
}
// Calculate percentage for first node to make equal splits
const splitPercent = 100 / nodes.length;
return {
direction: "column",
first: nodes[0],
second: buildVerticalStackOfNodes(nodes.slice(1)),
splitPercentage: splitPercent,
};
}
/**
* Calculates best grid dimensions for N windows
* Prefers square-ish grids, slightly favoring more columns than rows
*/
function calculateGridDimensions(windowCount: number): {
rows: number;
cols: number;
} {
const sqrt = Math.sqrt(windowCount);
const rows = Math.floor(sqrt);
const cols = Math.ceil(windowCount / rows);
return { rows, cols };
}
/**
* Chunks an array into groups of specified size
*/
function chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Builds a grid layout from window IDs
*/
function buildGridLayout(windowIds: string[]): MosaicNode<string> {
if (windowIds.length === 0) {
throw new Error("Cannot build grid with zero windows");
}
if (windowIds.length === 1) {
return windowIds[0];
}
const { cols } = calculateGridDimensions(windowIds.length);
// Split windows into rows
const rowChunks = chunkArray(windowIds, cols);
// Build each row as a horizontal split
const rowNodes = rowChunks.map((chunk) => buildHorizontalRow(chunk));
// Stack rows vertically
return buildVerticalStackOfNodes(rowNodes);
}
/**
* Built-in layout presets
*/
export const BUILT_IN_PRESETS: Record<string, LayoutPreset> = {
"side-by-side": {
id: "side-by-side",
name: "Side by Side",
description: "All windows in a single row",
minSlots: 2,
generate: (windowIds: string[]) => {
return buildHorizontalRow(windowIds);
},
},
"main-sidebar": {
id: "main-sidebar",
name: "Main + Sidebar",
description: "Large main window with sidebar windows stacked",
minSlots: 2,
generate: (windowIds: string[]) => {
const [main, ...sidebars] = windowIds;
if (sidebars.length === 0) {
return main;
}
return {
direction: "row",
first: main,
second: buildVerticalStack(sidebars),
splitPercentage: 70,
};
},
},
grid: {
id: "grid",
name: "Grid",
description: "All windows in an adaptive grid layout",
minSlots: 2,
generate: (windowIds: string[]) => {
return buildGridLayout(windowIds);
},
},
};
/**
* Collects window IDs from a layout tree in depth-first order
*/
export function collectWindowIds(layout: MosaicNode<string> | null): string[] {
if (layout === null) {
return [];
}
if (typeof layout === "string") {
return [layout];
}
return [
...collectWindowIds(layout.first),
...collectWindowIds(layout.second),
];
}
/**
* Applies a preset layout to existing windows
* Uses ALL windows in the adaptive layout
*/
export function applyPresetToLayout(
currentLayout: MosaicNode<string> | null,
preset: LayoutPreset,
): MosaicNode<string> {
// Collect all window IDs from current layout
const windowIds = collectWindowIds(currentLayout);
// Check minimum requirement
if (windowIds.length < preset.minSlots) {
throw new Error(
`Preset "${preset.name}" requires at least ${preset.minSlots} windows but only ${windowIds.length} available`,
);
}
// Check maximum limit if defined
if (preset.maxSlots && windowIds.length > preset.maxSlots) {
throw new Error(
`Preset "${preset.name}" supports maximum ${preset.maxSlots} windows but ${windowIds.length} available`,
);
}
// Generate layout using all windows
return preset.generate(windowIds);
}
/**
* Get a preset by ID
*/
export function getPreset(presetId: string): LayoutPreset | undefined {
return BUILT_IN_PRESETS[presetId];
}
/**
* Get all available presets
*/
export function getAllPresets(): LayoutPreset[] {
return Object.values(BUILT_IN_PRESETS);
}