Files
homarr/apps/nextjs/src/components/board/sections/container-section.tsx
T
Thomas 19760e5722 fix: restore translucent dashboard styling (#6664)
* fix: restore translucent dashboard styling

* fix: restore dashboard widget transparency

* fix: address dashboard review feedback

* fix: apply board opacity to containers

* fix: flatten demo board container

* fix: default board roundness to sm

* fix: restore assistant widget transparency

* Revert "fix: default board roundness to sm"

This reverts commit c235cbdf2b.

* fix: address remaining dashboard review comments

* test: align grid geometry expectations
2026-08-22 15:51:43 +02:00

159 lines
5.6 KiB
TypeScript

import dynamic from "next/dynamic";
import { ActionIcon, Badge, Box, Button, Card } from "@mantine/core";
import { IconChevronDown, IconChevronUp, IconExternalLink } from "@tabler/icons-react";
import combineClasses from "clsx";
import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useI18n } from "@homarr/translation/client";
import type { ContainerSectionItem } from "~/app/[locale]/boards/_types";
import { SectionGrid } from "./grid/section-grid";
import { useSectionCollapse } from "./section-collapse";
import { useOpenSectionApps } from "./use-open-section-apps";
import classes from "./item.module.css";
const BoardContainerMenu = dynamic(
() => import("./container/container-menu").then((module) => module.BoardContainerMenu),
{ ssr: false },
);
interface Props {
section: ContainerSectionItem;
}
export const BoardContainerSection = ({ section }: Props) => {
const board = useRequiredBoard();
const [isEditMode] = useEditMode();
const t = useI18n("section.container");
const tSection = useI18n("section");
const options = section.options;
const { open: openAllInNewTabs, isLoading: areAppsLoading } = useOpenSectionApps(
section.id,
options.showOpenAll && !isEditMode,
);
const { isVisuallyCollapsed, toggle } = useSectionCollapse({
sectionId: section.id,
collapsible: options.collapsible,
});
const label = options.title.trim() || t("untitled");
const contentId = `board-container-${section.id}-content`;
const labelLeft = 8;
const labelRight = isEditMode ? 48 : options.showOpenAll ? 40 : 8;
return (
<Box className="board-grid-item-content" data-grid-item-content w="100%" h="100%" style={{ overflow: "visible" }}>
<Card
className={combineClasses(
classes.itemCard,
classes.containerCard,
options.customCssClasses.join(" "),
isVisuallyCollapsed && classes.collapsedContainerCard,
)}
w="100%"
h="100%"
data-board-container-collapsed={isVisuallyCollapsed ? "true" : "false"}
styles={{
root: {
overflow: "visible",
"--opacity": board.opacity / 100,
"--container-border-color": options.borderColor || undefined,
},
}}
radius={board.itemRadius}
p={0}
>
{options.collapsible && (
<Button
className={classes.containerToggle}
pos="absolute"
top={isVisuallyCollapsed ? 0 : "calc(var(--mantine-spacing-xs) * -1)"}
left={0}
w="100%"
h={isVisuallyCollapsed ? "100%" : 24}
px={12}
pe={isEditMode ? 48 : options.showOpenAll ? 40 : 12}
radius="sm"
variant="default"
justify={options.showLabel ? "flex-start" : "center"}
leftSection={
options.showLabel ? (
isVisuallyCollapsed ? (
<IconChevronDown size={16} />
) : (
<IconChevronUp size={16} />
)
) : undefined
}
onClick={toggle}
aria-expanded={!isVisuallyCollapsed}
aria-controls={contentId}
aria-label={`${t(isVisuallyCollapsed ? "action.expand" : "action.collapse")}: ${label}`}
data-board-container-collapsed-control={isVisuallyCollapsed ? "true" : undefined}
data-board-container-label
title={options.showLabel ? label : undefined}
>
{options.showLabel ? (
label
) : isVisuallyCollapsed ? (
<IconChevronDown size={16} />
) : (
<IconChevronUp size={16} />
)}
</Button>
)}
{!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && (
<Badge
className={classes.containerLabel}
pos="absolute"
top="calc(var(--mantine-spacing-xs) * -1)"
left={labelLeft}
maw={`calc(100% - ${labelLeft + labelRight}px)`}
size="md"
radius="sm"
variant="default"
c="var(--mantine-color-text)"
style={{
zIndex: 9,
pointerEvents: "none",
}}
title={options.title}
data-board-container-label
>
{options.title}
</Badge>
)}
{options.showOpenAll && !isEditMode && (
<ActionIcon
className={classes.containerAction}
pos="absolute"
top={isVisuallyCollapsed ? "50%" : "calc(var(--mantine-spacing-xs) * -1)"}
right={8}
style={{ zIndex: 10, transform: isVisuallyCollapsed ? "translateY(-50%)" : undefined }}
variant={options.collapsible ? "subtle" : "default"}
size={24}
radius="sm"
loading={areAppsLoading}
onClick={openAllInNewTabs}
aria-label={tSection("action.openAllInNewTabsFor", { name: label })}
>
<IconExternalLink size={16} />
</ActionIcon>
)}
<Box
id={contentId}
className={classes.containerBody}
h="100%"
data-board-container-body
data-collapsed={isVisuallyCollapsed ? "true" : "false"}
aria-hidden={isVisuallyCollapsed}
inert={isVisuallyCollapsed}
>
<SectionGrid section={section} columnCount={section.width} requestedRowCount={section.height} label={label} />
</Box>
</Card>
{isEditMode && <BoardContainerMenu section={section} />}
</Box>
);
};