feat: rebuild onboarding experience

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:32:11 +02:00
1 parent d091362db2
commit e41605640b
249 files changed
+8216 -14060

No files matched your search

@@ -0,0 +1,68 @@
.canvas {
min-height: 8rem;
aspect-ratio: 16 / 7;
overflow: hidden;
color: var(--mantine-color-dimmed);
background:
radial-gradient(
circle at 15% 10%,
color-mix(in srgb, var(--mantine-primary-color-filled) 12%, transparent),
transparent 35%
),
var(--mantine-color-body);
}
.lanes {
display: grid;
height: 100%;
gap: rem(8px);
padding: rem(12px);
}
.lane {
display: grid;
min-width: 0;
gap: rem(3px);
padding: rem(4px);
overflow: hidden;
border: rem(1px) solid var(--mantine-color-default-border);
border-radius: var(--mantine-radius-sm);
background-image:
linear-gradient(
to right,
color-mix(in srgb, var(--mantine-color-dimmed) 8%, transparent) rem(1px),
transparent rem(1px)
),
linear-gradient(
to bottom,
color-mix(in srgb, var(--mantine-color-dimmed) 8%, transparent) rem(1px),
transparent rem(1px)
);
background-size: rem(12px) rem(12px);
}
.lane[data-lane="left"],
.lane[data-lane="right"] {
background-color: color-mix(in srgb, var(--mantine-primary-color-light) 40%, transparent);
}
.tile {
min-width: 0;
min-height: 0;
border: rem(1px) solid color-mix(in srgb, var(--mantine-primary-color-filled) 32%, transparent);
border-radius: max(rem(2px), calc(var(--mantine-radius-default) / 2));
background: color-mix(in srgb, var(--mantine-primary-color-filled) 18%, var(--mantine-color-body));
box-shadow: 0 rem(1px) rem(3px) rgb(0 0 0 / 8%);
}
.container {
border-style: dashed;
border-color: color-mix(in srgb, var(--mantine-color-teal-filled) 45%, transparent);
background: color-mix(in srgb, var(--mantine-color-teal-light) 55%, var(--mantine-color-body));
}
@media (max-width: $mantine-breakpoint-xs) {
.canvas {
min-height: 7rem;
}
}
@@ -0,0 +1,91 @@
import { Box, Center } 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 classes from "./board-layout-thumbnail.module.css";
interface BoardLayoutThumbnailProps {
preview: BoardPreviewData | null;
label: string;
}
const maxThumbnailRows = 12;
export const BoardLayoutThumbnail = ({ preview, label }: BoardLayoutThumbnailProps) => {
const layout = preview?.layouts.at(0);
if (!preview || !layout) {
return (
<Center className={classes.canvas} role="img" aria-label={label}>
<IconLayoutGrid size={28} stroke={1.4} aria-hidden />
</Center>
);
}
const elements = projectBoardLayout(preview, layout, layout);
const roots = preview.sections.filter((section) => section.kind === "empty");
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={classes.canvas} role="img" aria-label={label}>
<IconLayoutGrid size={28} stroke={1.4} aria-hidden />
</Center>
);
}
return (
<Box className={classes.canvas} 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 < maxThumbnailRows &&
element.width > 0 &&
element.height > 0,
);
const rowCount = Math.min(
maxThumbnailRows,
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) => (
<span
key={`${element.type}-${element.id}`}
aria-hidden
className={`${classes.tile} ${element.type === "section" ? classes.container : ""}`}
style={{
gridColumn: `${element.xOffset + 1} / span ${Math.min(element.width, columnCount - element.xOffset)}`,
gridRow: `${element.yOffset + 1} / span ${Math.min(element.height, rowCount - element.yOffset)}`,
}}
/>
))}
</div>
);
})}
</div>
</Box>
);
};
@@ -196,6 +196,7 @@ const ItemSelectModalContent = ({
search={search}
onSearchChange={setSearch}
placeholder={`${t("item.create.search")}...`}
ariaLabel={t("item.create.search")}
onSearchKeyDown={(event) => {
if (event.key === "Enter" && loadingSelection === null && filteredItems.length === 1) {
const [item] = filteredItems;