diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx
index f1eb3c572..1e4e47a4f 100644
--- a/apps/docs/docs/management/boards/index.mdx
+++ b/apps/docs/docs/management/boards/index.mdx
@@ -12,8 +12,6 @@ tags:
import iconColorImage from './img/icon-color.png'
import itemRadiusImage from './img/item-radius.png'
import layoutLargeImage from './img/layout-large.png'
-import layoutMediumImage from './img/layout-medium.png'
-import layoutSmallImage from './img/layout-small.png'
Each dashboard is called a "board".
You can create as many boards as you want.
@@ -76,32 +74,18 @@ Homarr will fall back to the Homarr logo if nothing is set.
### Layout
-In this section you can configure the different responsive layouts. Depending on your screens width the layout will change.
+In this section you can configure the number of columns in the desktop layout.
-
+Items and sections can be moved and resized on desktop while the board is in edit mode.
+Changing the column count automatically adjusts item positions and sizes to fit the new grid.
-On each element the position and size of items can be changed seperately.
-When adding a new layout the position and sizes will automatically be generated depending on the column count.
-When adding a new item / dynamic section that will always be 1x1 on each layout and placed on the first available position.
+
-#### Name
+#### Mobile layout
-The name of the layout can be choosen freely. It is only used for the management and will not be visible on the board.
+Mobile boards use an automatic, read-only layout with two columns. Items follow the visual order of the desktop layout and are limited to two columns wide and three rows high.
-#### Column count
-
-Amount of columns for this layout. Defines how many items can be placed in a row. When changed it will automatically adjust the position and size of the items.
-
-#### Breakpoint
-
-Minimum width of the screen where the layout will be used.
-Once the minmum of the next layout is reached the layout will change.
-
-#### Preview
-
-
-
-
+Categories and dynamic sections are not displayed as containers on mobile. Their items are included directly in the automatic grid, even when a category is collapsed on desktop. To rearrange, add, remove, or resize board items, open the board on a larger screen.
### Background
#### Background image URL
@@ -255,4 +239,4 @@ For the global home board you can only select boards that are public.
### Mobile home board
Same as the home board, you can also set a mobile home board.
-It will be used for devices that are detected as mobile devices. (Using User-Agent header)
\ No newline at end of file
+It will be used for devices that are detected as mobile devices. (Using User-Agent header)
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
index 0ba502a9a..eff57050a 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx
@@ -4,30 +4,37 @@ import { Box, Stack } from "@mantine/core";
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
+import { MobileBoard } from "~/components/board/mobile/mobile-board";
import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section";
+import { useIsMobileBoard } from "~/components/board/use-mobile-board";
import { BoardBackgroundVideo } from "~/components/layout/background";
export const ClientBoard = () => {
const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout();
+ const isMobile = useIsMobileBoard();
const fullWidthSortedSections = board.sections
.filter((section) => section.kind === "empty" || section.kind === "category")
- .sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
+ .toSorted((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
return (
-
- {fullWidthSortedSections.map((section) =>
- section.kind === "empty" ? (
-
- ) : (
-
- ),
- )}
-
+ {isMobile ? (
+
+ ) : (
+
+ {fullWidthSortedSections.map((section) =>
+ section.kind === "empty" ? (
+
+ ) : (
+
+ ),
+ )}
+
+ )}
);
};
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
index 30c91de40..822b83989 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx
@@ -38,6 +38,7 @@ import { useBoardPermissions } from "~/components/board/permissions/client";
import { useCategoryActions } from "~/components/board/sections/category/category-actions";
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions";
+import { useIsMobileBoard } from "~/components/board/use-mobile-board";
import { IntegrationSelectModal } from "~/components/integration/integration-select-modal";
import { HeaderButton } from "~/components/layout/header/button";
@@ -45,6 +46,7 @@ export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: bool
const [isEditMode] = useEditMode();
const board = useRequiredBoard();
const { hasChangeAccess } = useBoardPermissions(board);
+ const isMobile = useIsMobileBoard();
if (!hasChangeAccess) {
return ;
@@ -52,9 +54,9 @@ export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: bool
return (
<>
- {isEditMode && }
+ {isEditMode && !isMobile && }
-
+
{!demoReadOnly && (
@@ -157,7 +159,7 @@ const AddMenu = () => {
);
};
-const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => {
+const EditModeMenu = ({ demoReadOnly, hidden }: { demoReadOnly: boolean; hidden: boolean }) => {
const [isEditMode, { open, close }] = useEditMode();
const board = useRequiredBoard();
const utils = clientApi.useUtils();
@@ -186,16 +188,19 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => {
}, [utils, board.name, close]);
const toggle = useCallback(() => {
+ if (hidden) return;
if (isEditMode) {
if (demoReadOnly) return discardDemoChanges();
return saveBoard(board);
}
open();
- }, [board, isEditMode, demoReadOnly, saveBoard, open, discardDemoChanges]);
+ }, [board, isEditMode, demoReadOnly, saveBoard, open, discardDemoChanges, hidden]);
useHotkeys([[hotkeys.toggleBoardEdit, toggle]]);
usePreventLeaveWithDirty(isEditMode);
+ if (hidden) return null;
+
return (
diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx
index 97a576a30..ab7604fca 100644
--- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx
@@ -1,12 +1,12 @@
"use client";
-import { Button, Fieldset, Grid, Group, Input, NumberInput, Slider, Stack, Text, TextInput } from "@mantine/core";
+import { Button, Fieldset, Group, Input, Slider, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
-import { createId } from "@homarr/common";
+import { getDesktopLayout } from "@homarr/boards/context";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
-import { boardSaveLayoutsSchema } from "@homarr/validation/board";
+import { boardSaveLayoutSchema } from "@homarr/validation/board";
import type { Board } from "../../_types";
@@ -16,93 +16,43 @@ interface Props {
export const LayoutSettingsContent = ({ board }: Props) => {
const t = useI18n();
const utils = clientApi.useUtils();
- const { mutate: saveLayouts, isPending } = clientApi.board.saveLayouts.useMutation({
+ const desktopLayout = getDesktopLayout(board);
+ const { mutate: saveLayout, isPending } = clientApi.board.saveLayout.useMutation({
onSettled() {
void utils.board.getBoardByName.invalidate({ name: board.name });
void utils.board.getHomeBoard.invalidate();
},
});
- const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
+ const form = useZodForm(boardSaveLayoutSchema.omit({ id: true }).required(), {
initialValues: {
- layouts: board.layouts,
+ columnCount: desktopLayout.columnCount,
},
});
return (