From f133e09dc6d9036104337740a9552dd0fbbb954c Mon Sep 17 00:00:00 2001 From: ajnart Date: Thu, 23 Jul 2026 01:53:12 +0200 Subject: [PATCH 001/168] feat: add automatic mobile board layout --- apps/docs/docs/management/boards/index.mdx | 32 +--- .../app/[locale]/boards/(content)/_client.tsx | 27 +-- .../boards/(content)/_header-actions.tsx | 13 +- .../boards/[name]/settings/_layout.tsx | 92 +++------- .../board/mobile/mobile-board.module.css | 20 +++ .../components/board/mobile/mobile-board.tsx | 52 ++++++ .../board/mobile/mobile-layout.spec.ts | 85 ++++++++++ .../components/board/mobile/mobile-layout.ts | 106 ++++++++++++ .../src/components/board/use-mobile-board.ts | 7 + packages/api/src/router/board.ts | 160 ++++++------------ packages/api/src/router/test/board.spec.ts | 144 ++++------------ packages/boards/src/context.tsx | 42 ++--- packages/boards/src/edit-mode.tsx | 9 + packages/translation/src/lang/en.json | 11 +- packages/validation/src/board.ts | 11 +- 15 files changed, 441 insertions(+), 370 deletions(-) create mode 100644 apps/nextjs/src/components/board/mobile/mobile-board.module.css create mode 100644 apps/nextjs/src/components/board/mobile/mobile-board.tsx create mode 100644 apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts create mode 100644 apps/nextjs/src/components/board/mobile/mobile-layout.ts create mode 100644 apps/nextjs/src/components/board/use-mobile-board.ts 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. -![](./img/layout-settings.png) +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 && } - +