Files
homarr/apps/nextjs/src/components/board/board-layout-thumbnail.tsx
T

164 lines
6.5 KiB
TypeScript

import { Box, Center, Image } from "@mantine/core";
import { IconLayoutGrid } from "@tabler/icons-react";
import type { BoardPreviewData } from "@homarr/boards/layout-preview";
import { projectBoardLayout } from "@homarr/boards/layout-preview";
import { boardLanes, getBoardLaneColumnCount, getRootSectionLane } from "@homarr/definitions";
import { widgetCatalogIcons } from "@homarr/ui/widget-icons";
import classes from "./board-layout-thumbnail.module.css";
interface BoardLayoutThumbnailProps {
preview: BoardPreviewData | null;
label: string;
previewRowLimit?: number;
className?: string;
}
const compactBoardLayoutThumbnailRows = 12;
export const maxBoardLayoutThumbnailRows = 48;
export const BoardLayoutThumbnail = ({
preview,
label,
previewRowLimit = compactBoardLayoutThumbnailRows,
className,
}: BoardLayoutThumbnailProps) => {
const canvasClassName = [classes.canvas, className].filter(Boolean).join(" ");
const layout = preview?.layouts.find((candidate) => candidate.role === "base") ?? preview?.layouts.at(0);
if (!preview || !layout) {
return (
<Center className={canvasClassName} role="img" aria-label={label}>
<IconLayoutGrid size={28} stroke={1.4} aria-hidden />
</Center>
);
}
const elements = projectBoardLayout(preview, layout, layout);
const thumbnailRowLimit = Math.max(1, Math.min(Math.floor(previewRowLimit), maxBoardLayoutThumbnailRows));
const roots = preview.sections
.filter((section) => section.kind === "empty")
.toSorted((first, second) => (first.xOffset ?? 0) - (second.xOffset ?? 0) || first.id.localeCompare(second.id));
const lanes = boardLanes.flatMap((lane) => {
const root = roots.find((section) => getRootSectionLane(section.xOffset) === lane);
const columnCount = getBoardLaneColumnCount(layout, lane);
return root && columnCount > 0 ? [{ lane, root, columnCount }] : [];
});
if (lanes.length === 0) {
return (
<Center className={canvasClassName} role="img" aria-label={label}>
<IconLayoutGrid size={28} stroke={1.4} aria-hidden />
</Center>
);
}
const renderItemIcon = (itemId: string) => {
const item = preview.items.find((candidate) => candidate.id === itemId);
const WidgetIcon = item?.kind ? widgetCatalogIcons[item.kind] : undefined;
return item?.iconUrl ? (
<Image className={classes.itemIcon} src={item.iconUrl} alt="" fit="contain" />
) : WidgetIcon ? (
<WidgetIcon className={classes.itemIcon} stroke={1.7} />
) : null;
};
return (
<Box className={canvasClassName} role="img" aria-label={label}>
<div
className={classes.lanes}
style={{ gridTemplateColumns: lanes.map(({ columnCount }) => `${columnCount}fr`).join(" ") }}
>
{lanes.map(({ lane, root, columnCount }) => {
const laneElements = elements.filter(
(element) =>
element.sectionId === root.id &&
element.xOffset >= 0 &&
element.xOffset < columnCount &&
element.yOffset >= 0 &&
element.yOffset < thumbnailRowLimit &&
element.width > 0 &&
element.height > 0,
);
const rowCount = Math.min(
thumbnailRowLimit,
Math.max(4, ...laneElements.map((element) => element.yOffset + element.height)),
);
return (
<div
key={lane}
className={classes.lane}
data-lane={lane}
style={{
gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${rowCount}, minmax(0, 1fr))`,
}}
>
{laneElements.map((element) => {
const renderedWidth = Math.min(element.width, columnCount - element.xOffset);
const renderedHeight = Math.min(element.height, rowCount - element.yOffset);
const nestedItems =
element.type === "section"
? elements.filter(
(candidate) =>
candidate.type === "item" &&
candidate.sectionId === element.id &&
candidate.xOffset >= 0 &&
candidate.xOffset < renderedWidth &&
candidate.yOffset >= 0 &&
candidate.yOffset < renderedHeight &&
candidate.width > 0 &&
candidate.height > 0,
)
: [];
return (
<span
key={`${element.type}-${element.id}`}
aria-hidden
className={`${classes.tile} ${element.type === "section" ? classes.container : ""}`}
style={{
gridColumn: `${element.xOffset + 1} / span ${renderedWidth}`,
gridRow: `${element.yOffset + 1} / span ${renderedHeight}`,
}}
>
{element.type === "section" ? (
<span
className={classes.containerContents}
style={{
gridTemplateColumns: `repeat(${renderedWidth}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${renderedHeight}, minmax(0, 1fr))`,
}}
>
{nestedItems.map((nestedItem) => (
<span
key={nestedItem.id}
className={classes.tile}
style={{
gridColumn: `${nestedItem.xOffset + 1} / span ${Math.min(
nestedItem.width,
renderedWidth - nestedItem.xOffset,
)}`,
gridRow: `${nestedItem.yOffset + 1} / span ${Math.min(
nestedItem.height,
renderedHeight - nestedItem.yOffset,
)}`,
}}
>
{renderItemIcon(nestedItem.id)}
</span>
))}
</span>
) : (
renderItemIcon(element.id)
)}
</span>
);
})}
</div>
);
})}
</div>
</Box>
);
};