diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx index c975f9a9f..742e8b54d 100644 --- a/apps/docs/docs/management/api/index.mdx +++ b/apps/docs/docs/management/api/index.mdx @@ -62,6 +62,11 @@ Each board has exactly one Mobile and one Base layout. Mobile uses breakpoint `0 layouts can use any other breakpoint, and Homarr selects the highest breakpoint that fits the viewport. Preserve layout roles and use the canonical layouts returned by save operations, including generated IDs. +Board settings expose `fixedScaling` and `fixedItemSize`. The default fitted mode uses `fixedScaling: false`; setting it +to `true` keeps each `1 × 1` tile at `fixedItemSize` pixels, which must be an integer from `100` through `400`. The +configured size is retained while fixed scaling is disabled. Layout `columnCount` remains the maximum total column count, +including fixed sidebars. + Creating or changing an integration-backed widget requires use access to each newly selected integration. ## Docker targets diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 5b51ef96e..a9a105b20 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -27,9 +27,13 @@ Mobile and Base are protected layouts. New boards use three Mobile columns; Base creation. Additional layouts use unique breakpoints. Homarr selects the layout with the highest breakpoint that fits the current viewport. -Tiles use fixed `200 × 200` logical-pixel cells with a `24px` gap. Homarr scales the complete layout to the available -width instead of changing each widget's proportions. **Reset from Base** regenerates a Mobile or custom layout from the -current Base layout. +By default, Homarr fits the complete fixed-proportion tile grid to the available width. Enable **Fixed scaling** in the +board's Layout settings to render every `1 × 1` tile at the exact configured pixel size instead. A fixed dashboard is +centred when it fits and scrolls both horizontally and vertically when it does not. + +The column count is the maximum total for that layout, including any fixed sidebars. For example, a Base layout with +five columns and no sidebars remains a centred five-column dashboard on an ultrawide screen. **Reset from Base** +regenerates a Mobile or custom layout from the current Base layout. ### Containers and rails @@ -59,7 +63,7 @@ click remains available as a shortcut. Board settings cover: - **General:** page title, browser title, logo, and favicon. -- **Layout:** responsive layouts, breakpoints, columns, and rails. +- **Layout:** fitted or fixed scaling, tile size, responsive layouts, breakpoints, maximum columns, and rails. - **Background:** image, attachment, size, and repeat behavior. - **Appearance:** colors, opacity, icon color, and item radius. - **Custom CSS:** styles applied only to the current board. diff --git a/apps/docs/static/api/open-api-schema.json b/apps/docs/static/api/open-api-schema.json index 8a3df7032..45d0f5815 100644 --- a/apps/docs/static/api/open-api-schema.json +++ b/apps/docs/static/api/open-api-schema.json @@ -2151,6 +2151,14 @@ } ] }, + "fixedScaling": { + "type": "boolean" + }, + "fixedItemSize": { + "type": "integer", + "minimum": 100, + "maximum": 400 + }, "disableStatus": { "type": "boolean" } diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css index b60ab124a..c5574fc58 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css @@ -1,7 +1,6 @@ .columns { display: grid; align-items: start; - gap: 24px; } .lane { @@ -27,3 +26,10 @@ outline-offset: -0.0625rem; border-radius: var(--mantine-radius-md); } + +:global([data-canvas-scaling="fixed"]) .gutter { + --board-gutter-sticky-top: 0px; + --board-gutter-viewport-height: calc( + var(--board-canvas-fixed-viewport-height) * var(--board-canvas-inverse-scale, 1) + ); +} diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx index 5ecd8be66..c4bbe0bbd 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx @@ -18,7 +18,6 @@ import { getBoardLaneColumnCount, getInitialBoardLogicalHeight, getLogicalGridSize, - LOGICAL_GRID_GAP, getRootSectionForLane, } from "~/components/board/layout"; import { ScaledBoardCanvas } from "~/components/board/layout/scaled-board-canvas"; @@ -33,6 +32,7 @@ import { BoardSelectionToolbar } from "~/components/board/selection/board-select import classes from "./_client.module.css"; const APP_SHELL_INLINE_PADDING = 32; +const BOARD_LANE_GAP = 24; const BoardSelectionGridProvider = ({ children }: PropsWithChildren) => ( @@ -63,11 +63,11 @@ export const ClientBoard = () => { const laneWidths = [leftColumnCount, mainColumnCount, rightColumnCount] .filter((columnCount) => columnCount > 0) .map(getLogicalGridSize); - const logicalWidth = - laneWidths.reduce((total, width) => total + width, 0) + (laneWidths.length - 1) * LOGICAL_GRID_GAP; + const logicalWidth = laneWidths.reduce((total, width) => total + width, 0) + (laneWidths.length - 1) * BOARD_LANE_GAP; const initialLogicalHeight = getInitialBoardLogicalHeight(board, currentLayoutId); const representativeWidth = layoutOverrideId ? getRepresentativeLayoutWidth(currentLayout, board.layouts) : null; const initialAvailableWidth = Math.max(1, (representativeWidth ?? initialViewportWidth) - APP_SHELL_INLINE_PADDING); + const fixedItemSize = board.fixedScaling ? board.fixedItemSize : undefined; const gridTemplateColumns = [ leftColumnCount > 0 ? `${getLogicalGridSize(leftColumnCount)}px` : null, `${getLogicalGridSize(mainColumnCount)}px`, @@ -90,10 +90,11 @@ export const ClientBoard = () => { logicalWidth={logicalWidth} initialLogicalHeight={initialLogicalHeight} initialAvailableWidth={initialAvailableWidth} + fixedItemSize={fixedItemSize} label={board.name} > -
+
{leftColumnCount > 0 && leftSection && (