Files
homarr/packages/boards/src/layout-preview.ts
T

179 lines
5.4 KiB
TypeScript

import { generateResponsiveGridFor } from "@homarr/common";
import type { GridAlgorithmItem } from "@homarr/common";
import type { BoardLane, LayoutRole, WidgetKind } from "@homarr/definitions";
import { boardLanes, getBoardLaneColumnCount, getRootSectionLane } from "@homarr/definitions";
export interface BoardPreviewLayout {
id: string;
columnCount: number;
leftGutterColumnCount: number;
rightGutterColumnCount: number;
breakpoint: number;
role: LayoutRole;
}
interface BoardPreviewItemLayout {
layoutId: string;
sectionId: string;
xOffset: number;
yOffset: number;
width: number;
height: number;
}
interface BoardPreviewSectionLayout {
layoutId: string;
parentSectionId: string | null;
xOffset: number;
yOffset: number;
width: number;
height: number;
}
export interface BoardPreviewData {
layouts: readonly BoardPreviewLayout[];
sections: readonly BoardPreviewSection[];
items: readonly {
id: string;
kind?: WidgetKind;
iconUrl?: string | null;
layouts: readonly BoardPreviewItemLayout[];
}[];
}
type BoardPreviewSection =
| {
id: string;
kind: "empty";
xOffset: number | null;
layouts?: readonly BoardPreviewSectionLayout[];
}
| {
id: string;
kind: "container";
xOffset?: number | null;
layouts: readonly BoardPreviewSectionLayout[];
};
type LayoutGeometry = Pick<
BoardPreviewLayout,
"id" | "columnCount" | "leftGutterColumnCount" | "rightGutterColumnCount"
>;
export const getRepresentativeLayoutWidth = (layout: BoardPreviewLayout, allLayouts: readonly BoardPreviewLayout[]) => {
if (layout.role === "mobile") return 390;
if (layout.role === "base") return Math.max(1280, layout.breakpoint);
const nextLayout = allLayouts
.filter((candidate) => candidate.breakpoint > layout.breakpoint)
.toSorted((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint)
.at(0);
return nextLayout ? Math.round((layout.breakpoint + nextLayout.breakpoint) / 2) : Math.max(1280, layout.breakpoint);
};
export const projectBoardLayout = (
board: BoardPreviewData,
sourceLayout: LayoutGeometry,
targetLayout: LayoutGeometry,
) => {
const elements = getElementsForLayout(board, sourceLayout.id);
if (
sourceLayout.id === targetLayout.id &&
sourceLayout.columnCount === targetLayout.columnCount &&
sourceLayout.leftGutterColumnCount === targetLayout.leftGutterColumnCount &&
sourceLayout.rightGutterColumnCount === targetLayout.rightGutterColumnCount
) {
return elements;
}
const emptyRoots = board.sections
.filter((section) => section.kind === "empty")
.toSorted((first, second) => (first.xOffset ?? 0) - (second.xOffset ?? 0) || first.id.localeCompare(second.id));
const rootByLane = new Map<BoardLane, (typeof emptyRoots)[number]>();
for (const root of emptyRoots) {
const lane = getRootSectionLane(root.xOffset);
if (!rootByLane.has(lane)) rootByLane.set(lane, root);
}
const mainRoot = rootByLane.get("main");
if (!mainRoot) return [];
const sourceRootById = new Map(
emptyRoots.map((section) => [section.id, getRootSectionLane(section.xOffset)] as const),
);
const targetLaneBySourceLane = new Map<BoardLane, BoardLane>(
boardLanes.map((lane) => [
lane,
lane !== "main" && getBoardLaneColumnCount(targetLayout, lane) === 0 ? "main" : lane,
]),
);
const remappedElements = elements.map((element) => {
const sourceLane = sourceRootById.get(element.sectionId);
if (!sourceLane) return element;
const targetLane = targetLaneBySourceLane.get(sourceLane) ?? "main";
return { ...element, sectionId: (rootByLane.get(targetLane) ?? mainRoot).id };
});
return boardLanes.flatMap((lane) => {
const root = rootByLane.get(lane);
const width = getBoardLaneColumnCount(targetLayout, lane);
if (!root || width === 0) return [];
const sourceLanes = boardLanes.filter(
(sourceLane) =>
rootByLane.has(sourceLane) &&
getBoardLaneColumnCount(sourceLayout, sourceLane) > 0 &&
targetLaneBySourceLane.get(sourceLane) === lane,
);
const previousWidth =
sourceLanes.length === 1
? getBoardLaneColumnCount(sourceLayout, sourceLanes[0] ?? "main")
: Number.MAX_SAFE_INTEGER;
return generateResponsiveGridFor({
items: remappedElements,
previousWidth,
width,
sectionId: root.id,
}).items;
});
};
const getElementsForLayout = (board: BoardPreviewData, layoutId: string): GridAlgorithmItem[] => [
...board.items.flatMap((item) => {
const layout = item.layouts.find((candidate) => candidate.layoutId === layoutId);
return layout
? [
{
id: item.id,
type: "item" as const,
height: layout.height,
width: layout.width,
xOffset: layout.xOffset,
yOffset: layout.yOffset,
sectionId: layout.sectionId,
},
]
: [];
}),
...board.sections.flatMap((section) => {
if (section.kind !== "container") return [];
const layout = section.layouts.find((candidate) => candidate.layoutId === layoutId);
return layout?.parentSectionId
? [
{
id: section.id,
type: "section" as const,
height: layout.height,
width: layout.width,
xOffset: layout.xOffset,
yOffset: layout.yOffset,
sectionId: layout.parentSectionId,
},
]
: [];
}),
];