feat: add fixed board scaling

This commit is contained in:
ajnart committed 2026-09-08 12:37:51 +02:00
1 parent 9d857377c1
commit ecc1009e23
36 files changed
+8654 -41

No files matched your search

+10 -4
View File
@@ -42,6 +42,8 @@ import {
getWidgetIntegrationIssueMessage,
getPermissionsWithChildren,
getPermissionsWithParents,
BOARD_FIXED_ITEM_SIZE_MAX,
BOARD_FIXED_ITEM_SIZE_MIN,
normalizeBoardLayoutRoles,
rootSectionOffsets,
widgetDefaultSizes,
@@ -1142,8 +1144,7 @@ export const boardRouter = createTRPCRouter({
.meta({
mcp: {
enabled: true,
description:
"Read the editable visual and behavior settings for one board, including its current custom CSS. Requires modify permission. REQUIRED: id (board ID). Call this before proposing board settings or custom CSS changes",
description: `Read the editable visual and behavior settings for one board, including its current custom CSS and board scaling settings. fixedScaling keeps each 1x1 grid item at fixedItemSize pixels instead of fitting the board to the viewport; fixedItemSize is ${BOARD_FIXED_ITEM_SIZE_MIN}-${BOARD_FIXED_ITEM_SIZE_MAX}. Requires modify permission. REQUIRED: id (board ID). Call this before proposing board settings or custom CSS changes`,
},
})
.input(z.object({ id: z.string() }))
@@ -1170,6 +1171,8 @@ export const boardRouter = createTRPCRouter({
customCss: true,
iconColor: true,
itemRadius: true,
fixedScaling: true,
fixedItemSize: true,
disableStatus: true,
},
where: boardWhere,
@@ -1526,8 +1529,7 @@ export const boardRouter = createTRPCRouter({
openapi: { method: "PATCH", path: "/api/boards/{id}/settings", tags: ["boards"], protect: true },
mcp: {
enabled: true,
description:
"Update visual and behavior settings for a board. Requires modify permission. REQUIRED: id (board ID). Optional fields include pageTitle, metaTitle, logoImageUrl, faviconImageUrl, backgroundImageUrl, colors, opacity, customCss, itemRadius, and disableStatus",
description: `Update visual and behavior settings for a board. Set fixedScaling to true to keep each 1x1 grid item at fixedItemSize pixels instead of fitting the board to the viewport; fixedItemSize must be ${BOARD_FIXED_ITEM_SIZE_MIN}-${BOARD_FIXED_ITEM_SIZE_MAX}. Requires modify permission. REQUIRED: id (board ID). Optional fields include pageTitle, metaTitle, logoImageUrl, faviconImageUrl, backgroundImageUrl, colors, opacity, customCss, itemRadius, fixedScaling, fixedItemSize, and disableStatus`,
},
})
.input(boardSavePartialSettingsSchema.extend({ id: z.string() }))
@@ -1557,6 +1559,10 @@ export const boardRouter = createTRPCRouter({
iconColor: input.iconColor,
itemRadius: input.itemRadius,
// scaling settings
fixedScaling: input.fixedScaling,
fixedItemSize: input.fixedItemSize,
// custom css
customCss: input.customCss,
@@ -29,6 +29,7 @@ import {
serverSettings,
} from "@homarr/db/schema";
import {
BOARD_FIXED_ITEM_SIZE_DEFAULT,
emptySuperJSON,
everyoneGroup,
extractContainerImageName,
@@ -388,6 +389,8 @@ export const onboardRouter = createTRPCRouter({
customCss: null,
iconColor: null,
itemRadius: input.board.itemRadius,
fixedScaling: false,
fixedItemSize: BOARD_FIXED_ITEM_SIZE_DEFAULT,
disableStatus: false,
sections: [],
layouts: pendingLayouts,
@@ -0,0 +1,2 @@
ALTER TABLE `board` ADD `fixed_scaling` boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE `board` ADD `fixed_item_size` smallint DEFAULT 200 NOT NULL;
File diff suppressed because it is too large. Load diff
@@ -351,6 +351,13 @@
"when": 1788763848931,
"tag": "0049_add_byte_unit_system",
"breakpoints": true
},
{
"idx": 50,
"version": "5",
"when": 1788845117219,
"tag": "0050_add_fixed_board_scaling",
"breakpoints": true
}
]
}
@@ -0,0 +1,2 @@
ALTER TABLE "board" ADD COLUMN "fixed_scaling" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "board" ADD COLUMN "fixed_item_size" smallint DEFAULT 200 NOT NULL;
File diff suppressed because it is too large. Load diff
@@ -113,6 +113,13 @@
"when": 1788763848932,
"tag": "0015_add_byte_unit_system",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1788845119083,
"tag": "0016_add_fixed_board_scaling",
"breakpoints": true
}
]
}
@@ -0,0 +1,2 @@
ALTER TABLE `board` ADD `fixed_scaling` integer DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE `board` ADD `fixed_item_size` integer DEFAULT 200 NOT NULL;
File diff suppressed because it is too large. Load diff
@@ -337,6 +337,13 @@
"when": 1788763848930,
"tag": "0047_add_byte_unit_system",
"breakpoints": true
},
{
"idx": 48,
"version": "6",
"when": 1788845055278,
"tag": "0048_add_fixed_board_scaling",
"breakpoints": true
}
]
}
+3
View File
@@ -23,6 +23,7 @@ import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
BOARD_FIXED_ITEM_SIZE_DEFAULT,
emptySuperJSON,
} from "@homarr/definitions";
import type {
@@ -301,6 +302,8 @@ export const boards = mysqlTable("board", {
customCss: text(),
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
fixedScaling: boolean().default(false).notNull(),
fixedItemSize: smallint().default(BOARD_FIXED_ITEM_SIZE_DEFAULT).notNull(),
disableStatus: boolean().default(false).notNull(),
});
+3
View File
@@ -21,6 +21,7 @@ import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
BOARD_FIXED_ITEM_SIZE_DEFAULT,
emptySuperJSON,
} from "@homarr/definitions";
import type {
@@ -299,6 +300,8 @@ export const boards = pgTable("board", {
customCss: text(),
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
fixedScaling: boolean().default(false).notNull(),
fixedItemSize: smallint().default(BOARD_FIXED_ITEM_SIZE_DEFAULT).notNull(),
disableStatus: boolean().default(false).notNull(),
});
+3
View File
@@ -9,6 +9,7 @@ import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
BOARD_FIXED_ITEM_SIZE_DEFAULT,
emptySuperJSON,
} from "@homarr/definitions";
import type {
@@ -284,6 +285,8 @@ export const boards = sqliteTable("board", {
customCss: text(),
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
fixedScaling: int({ mode: "boolean" }).default(false).notNull(),
fixedItemSize: int().default(BOARD_FIXED_ITEM_SIZE_DEFAULT).notNull(),
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
});
+4
View File
@@ -10,6 +10,10 @@ export const backgroundImageSizes = createDefinition(["cover", "contain"], {
});
export const layoutRoles = createDefinition(["mobile", "base", "custom"], { defaultValue: "custom" });
export const BOARD_FIXED_ITEM_SIZE_DEFAULT = 200;
export const BOARD_FIXED_ITEM_SIZE_MIN = 100;
export const BOARD_FIXED_ITEM_SIZE_MAX = 400;
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
+16 -1
View File
@@ -5430,6 +5430,16 @@
"label": "Disable app status",
"description": "Disables the status check for all apps on this board"
},
"fixedScaling": {
"label": "Fixed scaling",
"description": "Keep every tile at the selected size. The dashboard is centred when it fits and scrolls when it does not."
},
"fixedItemSize": {
"label": "1 × 1 tile size",
"description": "Set the exact visible width and height used for a 1 × 1 tile when fixed scaling is enabled.",
"previewLabel": "1 by 1 tile preview at {size} by {size} pixels",
"previewValue": "{size} x {size} px"
},
"columnCount": {
"label": "Column count"
},
@@ -5461,6 +5471,9 @@
},
"layout": {
"title": "Layout",
"scaling": {
"title": "Scaling"
},
"responsive": {
"title": "Responsive layouts",
"description": "Each layout keeps its own widget positions and sizes. Mobile and Base are always available; add custom layouts at any additional breakpoint.",
@@ -5607,7 +5620,8 @@
"layout": {
"field": {
"columnCount": {
"label": "Column count"
"label": "Column count",
"description": "Maximum total columns in this layout, including fixed sidebars."
},
"breakpoint": {
"label": "Breakpoint",
@@ -8032,6 +8046,7 @@
"resetAll": "Reset all changes",
"tabs": {
"css": "Custom CSS",
"layout": "Layout",
"appearance": "Appearance",
"background": "Background",
"general": "General"
+4
View File
@@ -6,6 +6,8 @@ import {
backgroundImageSizes,
boardPermissions,
layoutRoles,
BOARD_FIXED_ITEM_SIZE_MAX,
BOARD_FIXED_ITEM_SIZE_MIN,
widgetKinds,
} from "@homarr/definitions";
@@ -68,6 +70,8 @@ export const boardSavePartialSettingsSchema = z
customCss: z.string().max(16384),
iconColor: hexColorNullableSchema,
itemRadius: z.union([z.literal("xs"), z.literal("sm"), z.literal("md"), z.literal("lg"), z.literal("xl")]),
fixedScaling: z.boolean(),
fixedItemSize: z.number().int().min(BOARD_FIXED_ITEM_SIZE_MIN).max(BOARD_FIXED_ITEM_SIZE_MAX),
disableStatus: z.boolean(),
})
.partial();