From 6db134cf1527713f2b3455ebb602865dd8844590 Mon Sep 17 00:00:00 2001 From: ajnart Date: Tue, 8 Sep 2026 16:17:01 +0200 Subject: [PATCH] feat: refine board settings and compact previews --- apps/docs/docs/management/boards/index.mdx | 31 +- .../app/[locale]/boards/(content)/_client.tsx | 19 +- .../settings/_appearance-playground.tsx | 51 +++ .../boards/[name]/settings/_appereance.tsx | 29 +- .../boards/[name]/settings/_background.tsx | 18 +- .../settings/_layout-preview.module.css | 70 +--- .../[name]/settings/_layout-preview.tsx | 148 +++----- .../boards/[name]/settings/_layout.module.css | 38 +- .../boards/[name]/settings/_layout.tsx | 358 +++++++++--------- .../boards/[name]/settings/_scaling.tsx | 103 +++++ .../[name]/settings/_settings-form.module.css | 43 +++ .../boards/[name]/settings/_settings-form.tsx | 145 +++++-- .../assistant-board-settings-tool.tsx | 4 + .../board/board-layout-thumbnail-canvas.tsx | 210 ++++++++++ .../board/board-layout-thumbnail.module.css | 63 ++- .../board/board-layout-thumbnail.tsx | 7 + .../src/components/board/layout/README.md | 18 + .../board/layout/canvas-appearance.module.css | 39 ++ .../board/layout/canvas-geometry.ts | 22 ++ .../board/layout/canvas-scrollbar.module.css | 35 ++ .../layout/scaled-board-canvas.module.css | 79 +--- .../board/layout/scaled-board-canvas.tsx | 62 ++- .../src/components/board/layout/scaling.ts | 55 +++ .../layout/test/scaled-board-canvas.spec.ts | 59 ++- .../board/layout/use-canvas-viewport-size.ts | 16 + .../board/sections/grid/section-grid.tsx | 7 +- packages/translation/src/lang/en.json | 24 +- 27 files changed, 1203 insertions(+), 550 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/boards/[name]/settings/_appearance-playground.tsx create mode 100644 apps/nextjs/src/app/[locale]/boards/[name]/settings/_scaling.tsx create mode 100644 apps/nextjs/src/app/[locale]/boards/[name]/settings/_settings-form.module.css create mode 100644 apps/nextjs/src/components/board/board-layout-thumbnail-canvas.tsx create mode 100644 apps/nextjs/src/components/board/layout/canvas-appearance.module.css create mode 100644 apps/nextjs/src/components/board/layout/canvas-geometry.ts create mode 100644 apps/nextjs/src/components/board/layout/canvas-scrollbar.module.css create mode 100644 apps/nextjs/src/components/board/layout/scaling.ts create mode 100644 apps/nextjs/src/components/board/layout/use-canvas-viewport-size.ts diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index a9a105b20..95434213e 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -28,7 +28,7 @@ creation. Additional layouts use unique breakpoints. Homarr selects the layout w the current viewport. 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 +board's Layout settings to render each top-level `1 × 1` tile at the configured CSS-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 @@ -60,14 +60,29 @@ click remains available as a shortcut. ## Board settings -Board settings cover: +Settings are organized into tabs, with one **Save changes** action for the whole board. Switching tabs retains your +draft; **Discard** restores the saved settings. -- **General:** page title, browser title, logo, and favicon. -- **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. -- **Behaviour:** app status and widget context-menu behavior. +- **Layout:** tile sizing, responsive layouts, breakpoints, maximum columns, and rails. +- **Appearance:** colors, opacity, icon color, item radius, and background image settings beside a live layout preview. +- **General:** page title, browser title, logo, favicon, and app status. +- **Advanced:** custom CSS applied only to this board. +- **Access control:** permissions, visibility, renaming, and deletion (requires full access). + +### Try a layout before saving + +Choose **Fixed scaling** or **Fit to screen** under **Tile sizing**. Fixed sizing accepts **100–400 px**; its square +sample is shown at half size with the configured dimensions underneath. The size is retained when you switch modes. + +Select a layout to edit its columns, breakpoint, and side rails beside the compact **Board preview**. It uses the +familiar Boards thumbnail, with the board's actual grid proportions and rails aligned beside the main canvas. +The indicated screen width represents the selected layout. The miniature stays contained, centres fixed grids that +fit, and scrolls when needed. Changes to sizing and appearance appear immediately, before saving. + +In fixed mode, browser `Cmd`/`Ctrl` + `+` or `-` zoom works naturally: a 200 × 200 CSS-pixel tile keeps those dimensions +while its physical screen size changes with zoom. Homarr does not shrink the tile to cancel the browser's zoom. +Your live board can still select a different saved layout when its viewport crosses a breakpoint. Nested Containers +fit their inner grids into their own frames; the configured tile size describes the top-level board grid. See [Styling](/docs/advanced/styling) for board and global CSS. diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx index c4bbe0bbd..670b908cc 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx @@ -17,10 +17,11 @@ import { BoardSetupChecklist } from "~/components/board/board-setup-checklist"; import { getBoardLaneColumnCount, getInitialBoardLogicalHeight, - getLogicalGridSize, getRootSectionForLane, } from "~/components/board/layout"; import { ScaledBoardCanvas } from "~/components/board/layout/scaled-board-canvas"; +import { BOARD_LANE_GAP, getBoardCanvasGeometry } from "~/components/board/layout/canvas-geometry"; +import { getBoardScaling } from "~/components/board/layout/scaling"; import { BoardEmptySection } from "~/components/board/sections/empty-section"; import { BoardGridEditorBoundary } from "~/components/board/sections/grid/board-grid-editor-boundary"; import { GridEditorRegistryProvider } from "~/components/board/sections/grid/grid-editor-registry"; @@ -32,7 +33,6 @@ 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) => ( @@ -60,21 +60,10 @@ export const ClientBoard = () => { const leftColumnCount = getBoardLaneColumnCount(currentLayout, "left"); const mainColumnCount = getBoardLaneColumnCount(currentLayout, "main"); const rightColumnCount = getBoardLaneColumnCount(currentLayout, "right"); - const laneWidths = [leftColumnCount, mainColumnCount, rightColumnCount] - .filter((columnCount) => columnCount > 0) - .map(getLogicalGridSize); - const logicalWidth = laneWidths.reduce((total, width) => total + width, 0) + (laneWidths.length - 1) * BOARD_LANE_GAP; + const { width: logicalWidth, gridTemplateColumns } = getBoardCanvasGeometry(currentLayout); 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`, - rightColumnCount > 0 ? `${getLogicalGridSize(rightColumnCount)}px` : null, - ] - .filter((value) => value !== null) - .join(" "); const content = ( @@ -90,7 +79,7 @@ export const ClientBoard = () => { logicalWidth={logicalWidth} initialLogicalHeight={initialLogicalHeight} initialAvailableWidth={initialAvailableWidth} - fixedItemSize={fixedItemSize} + scaling={getBoardScaling(board)} label={board.name} > diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appearance-playground.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appearance-playground.tsx new file mode 100644 index 000000000..9afbcbf73 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appearance-playground.tsx @@ -0,0 +1,51 @@ +import { Stack } from "@mantine/core"; + +import type { RouterOutputs } from "@homarr/api"; +import type { UseFormReturnType } from "@homarr/form"; + +import type { Board } from "../../_types"; +import { ColorSettingsContent } from "./_appereance"; +import { BackgroundSettingsContent } from "./_background"; +import { LayoutPreview } from "./_layout-preview"; +import type { FormValues } from "./_settings-form"; +import classes from "./_settings-form.module.css"; + +export const AppearancePlayground = ({ + board, + form, + apps, + selectedLayoutId, +}: { + board: Board; + form: UseFormReturnType; + apps: RouterOutputs["app"]["byIds"]; + selectedLayoutId: string | null; +}) => { + const layout = + form.values.layouts.find((candidate) => candidate.id === selectedLayoutId) ?? + form.values.layouts.find((candidate) => candidate.role === "base"); + const sourceLayout = + board.layouts.find((candidate) => candidate.id === layout?.id) ?? + board.layouts.find((candidate) => candidate.role === "base"); + + return ( +
+ + + + + {layout && sourceLayout && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx index 1e5e07ce4..d8de8ff97 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx @@ -1,7 +1,6 @@ "use client"; import { - Anchor, Button, Collapse, ColorInput, @@ -39,7 +38,6 @@ const progressPercentageLabel = (value: number) => `${value}%`; export const ColorSettingsContent = ({ form }: Props) => { const [showPreview, { toggle }] = useDisclosure(false); const tBoard = useI18n("board"); - const tCommon = useI18n("common"); const theme = useMantineTheme(); const { branding } = useSettings(); @@ -50,6 +48,11 @@ export const ColorSettingsContent = ({ form }: Props) => { { - + - {showPreview ? tCommon("preview.hide") : tCommon("preview.show")} + @@ -73,6 +88,7 @@ export const ColorSettingsContent = ({ form }: Props) => { { color[6])} flex={1} diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx index a24759ad5..96264fbde 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_background.tsx @@ -1,7 +1,7 @@ "use client"; import { startTransition } from "react"; -import { ActionIcon, Autocomplete, Center, Grid, Group, Popover, Stack, Text } from "@mantine/core"; +import { ActionIcon, Autocomplete, Grid, Group, Popover, Stack, Text } from "@mantine/core"; import { useDebouncedValue } from "@mantine/hooks"; import { IconPhotoOff, IconUpload } from "@tabler/icons-react"; @@ -24,6 +24,7 @@ interface Props { export const BackgroundSettingsContent = ({ form }: Props) => { const tBoard = useI18n("board"); + const tMedia = useI18n("media"); const { data: session } = useSession(); const [debouncedSearch] = useDebouncedValue(form.values.backgroundImageUrl ?? "", 200); @@ -50,14 +51,15 @@ export const BackgroundSettingsContent = ({ form }: Props) => { = 2 && ( -
+ -
+
@@ -113,7 +115,15 @@ export const BackgroundSettingsContent = ({ form }: Props) => { }} > {({ onClick, loading }) => ( - + )} diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.module.css b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.module.css index f8415e012..3ac77e667 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.module.css +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.module.css @@ -1,70 +1,6 @@ -.canvas { - border: 1px solid var(--mantine-color-default-border); - border-radius: var(--mantine-radius-md); - background: color-mix(in srgb, var(--mantine-color-body) 82%, var(--mantine-primary-color-light)); - padding: var(--mantine-spacing-xs); - min-height: 7rem; - max-height: min(18rem, 38dvh); - overflow-y: auto; - overscroll-behavior: contain; - scrollbar-gutter: stable; -} - -.section { - border-radius: var(--mantine-radius-sm); - background: color-mix(in srgb, var(--mantine-color-default) 78%, transparent); - padding: 0.35rem; -} - -.grid { - display: grid; - gap: 3px; - grid-auto-rows: clamp(1.35rem, 2.2vw, 1.75rem); - min-height: 2.5rem; -} - -.tile { - container-type: inline-size; - min-width: 0; - overflow: hidden; - border: 1px solid color-mix(in srgb, var(--mantine-primary-color-filled) 24%, var(--mantine-color-default-border)); - border-radius: calc(var(--mantine-radius-sm) * 0.75); - background: color-mix(in srgb, var(--mantine-primary-color-light) 70%, var(--mantine-color-body)); - padding-inline: 0.25rem; -} - -.tileContent { - justify-content: center; +.preview { width: 100%; - height: 100%; - overflow: hidden; -} - -.tileLabel { - display: none; + max-width: 32rem; min-width: 0; - max-height: 2.1em; - overflow: hidden; - overflow-wrap: anywhere; - line-height: 1.05; -} - -@container (min-width: 9rem) { - .tileContent { - justify-content: flex-start; - } - - .tileLabel { - display: block; - } -} - -.containerTile { - border-style: dashed; - background: color-mix(in srgb, var(--mantine-color-default) 88%, var(--mantine-primary-color-light)); -} - -.appIcon { - width: 0.85rem; - height: 0.85rem; + margin-inline: auto; } diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.tsx index ef32bfa2a..108a27fd0 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout-preview.tsx @@ -1,17 +1,17 @@ "use client"; -import { Badge, Box, Group, Stack, Text, ThemeIcon, Tooltip } from "@mantine/core"; -import { IconLayoutGrid } from "@tabler/icons-react"; +import { useMemo } from "react"; +import { Badge, ColorSwatch, Group, Stack, Text, Tooltip } from "@mantine/core"; import type { RouterOutputs } from "@homarr/api"; import type { BoardPreviewLayout } from "@homarr/boards/layout-preview"; -import { getRepresentativeLayoutWidth, projectBoardLayout } from "@homarr/boards/layout-preview"; -import { getBoardLaneColumnCount, getRootSectionLane, getWidgetName } from "@homarr/definitions"; +import { getRepresentativeLayoutWidth } from "@homarr/boards/layout-preview"; import { useI18n } from "@homarr/translation/client"; -import { MaskedOrNormalImage } from "@homarr/ui"; -import { widgetCatalogIcons } from "@homarr/ui/widget-icons"; +import { BoardLayoutThumbnail } from "~/components/board/board-layout-thumbnail"; +import { normalizeFixedItemSize } from "~/components/board/layout/scaling"; import type { Board } from "../../_types"; +import type { FormValues } from "./_settings-form"; import classes from "./_layout-preview.module.css"; interface Props { @@ -20,107 +20,55 @@ interface Props { layouts: BoardPreviewLayout[]; sourceLayout: BoardPreviewLayout; apps: RouterOutputs["app"]["byIds"]; + settings: FormValues; } -export const LayoutPreview = ({ board, layout, layouts, sourceLayout, apps }: Props) => { +export const LayoutPreview = ({ board, layout, layouts, sourceLayout, apps, settings }: Props) => { + const t = useI18n("board.setting.section.layout.preview"); const tBoard = useI18n("board"); - const tSection = useI18n("section"); - const t = useI18n(); - const elements = projectBoardLayout(board, sourceLayout, layout); - const representativeWidth = getRepresentativeLayoutWidth(layout, layouts); - const largestRepresentativeWidth = Math.max( - ...layouts.map((candidate) => getRepresentativeLayoutWidth(candidate, layouts)), - ); - const previewWidth = `${(representativeWidth / largestRepresentativeWidth) * 100}%`; - const appsById = new Map(apps.map((app) => [app.id, app])); - const rootSections = board.sections - .filter((section) => section.kind === "empty") - .toSorted((sectionA, sectionB) => sectionA.xOffset - sectionB.xOffset); + const preview = useMemo(() => { + const appsById = new Map(apps.map((app) => [app.id, app])); + return { + layouts, + sections: board.sections, + items: board.items.map((item) => { + let iconUrl: string | undefined; + if (item.kind === "app" && typeof item.options.appId === "string") + iconUrl = appsById.get(item.options.appId)?.iconUrl; + return { ...item, iconUrl }; + }), + }; + }, [board, layouts, apps]); return ( - - - - {representativeWidth}px - - - {layout.columnCount} {tBoard("setting.section.layout.preview.columns")} + + + + {t("title")} + + {Math.max(320, getRepresentativeLayoutWidth(layout, layouts))} px + - - {elements.length === 0 ? ( - - {tBoard("setting.section.layout.preview.empty")} - - ) : ( - - {rootSections.map((section) => { - const sectionElements = elements.filter((element) => element.sectionId === section.id); - const columnCount = getBoardLaneColumnCount(layout, getRootSectionLane(section.xOffset)); - if (sectionElements.length === 0 || columnCount === 0) return null; - - return ( - - - {sectionElements.map((element) => { - const item = - element.type === "item" ? board.items.find((candidate) => candidate.id === element.id) : null; - const appId = - item?.kind === "app" && typeof item.options.appId === "string" ? item.options.appId : null; - const app = appId ? appsById.get(appId) : undefined; - const container = - element.type === "section" - ? board.sections.find((candidate) => candidate.id === element.id) - : undefined; - const containerLabel = - container?.kind === "container" && container.options.title - ? container.options.title - : tSection("container.untitled"); - const label = app?.name ?? (item ? getWidgetName(item.kind, t) : containerLabel); - const WidgetIcon = item ? widgetCatalogIcons[item.kind] : IconLayoutGrid; - - return ( - - - - - {app ? ( - - ) : ( - - )} - - - {label} - - - - - ); - })} - - - ); - })} - - )} - + + + + {settings.fixedScaling + ? t("dimensions", { size: normalizeFixedItemSize(settings.fixedItemSize), columns: layout.columnCount }) + : t("responsiveDimensions", { columns: layout.columnCount })} + + + + + + + + + + + + {t("description")} + ); }; diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.module.css b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.module.css index a7994fee9..74e0fe5cb 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.module.css +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.module.css @@ -1,29 +1,53 @@ -.scalingSettings, .gutterSettings { background: color-mix(in srgb, var(--mantine-color-default) 55%, transparent); } +.scalingSettings { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(220px, 0.6fr); + gap: var(--mantine-spacing-xl); + padding: var(--mantine-spacing-lg); + border: 1px solid var(--mantine-color-default-border); + border-radius: var(--mantine-radius-md); + background: var(--mantine-color-body); +} + +.scalingControls { + align-self: center; + min-width: 0; +} + .fixedItemSizePreview { display: flex; + flex-direction: column; align-items: center; + justify-content: center; gap: var(--mantine-spacing-md); margin: 0; - min-width: 12rem; + min-width: 0; + min-height: 200px; } .fixedItemSizePreviewTile { display: grid; flex: 0 0 auto; place-items: center; - min-width: 1.5rem; - min-height: 1.5rem; color: var(--mantine-primary-color-contrast); background: var(--mantine-primary-color-filled); border: 1px solid var(--mantine-primary-color-filled-hover); border-radius: var(--mantine-radius-md); - transition: - width 180ms ease-out, - height 180ms ease-out; +} + +@media (max-width: 48em) { + .scalingSettings { + grid-template-columns: minmax(0, 1fr); + } +} + +@media (max-width: 62em) { + .layoutPreview { + order: -1; + } } .gutterPreview { 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 f4cf39bc4..9cf74e844 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_layout.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useRouter } from "next/navigation"; import { Badge, @@ -12,6 +12,8 @@ import { Input, NumberInput, Paper, + Select, + Alert, Slider, Stack, Switch, @@ -21,8 +23,8 @@ import { import { IconEdit, IconRefresh } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import type { RouterOutputs } from "@homarr/api"; import { createId } from "@homarr/common"; -import { BOARD_FIXED_ITEM_SIZE_MAX, BOARD_FIXED_ITEM_SIZE_MIN } from "@homarr/definitions"; import type { UseFormReturnType } from "@homarr/form"; import { showErrorNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; @@ -31,6 +33,7 @@ import { InlineConfirmButton } from "@homarr/ui"; import { SectionCard } from "~/components/manage/section-card"; import type { Board } from "../../_types"; import { LayoutPreview } from "./_layout-preview"; +import { ScalingSettings } from "./_scaling"; import type { FormValues } from "./_settings-form"; import classes from "./_layout.module.css"; @@ -39,11 +42,22 @@ interface Props { form: UseFormReturnType; isSaving: boolean; saveSettingsAsync: () => Promise; + selectedLayoutId: string | null; + setSelectedLayoutId: (value: string | null) => void; + apps: RouterOutputs["app"]["byIds"]; } const layoutRoleOrder = { base: 0, custom: 1, mobile: 2 } as const; -export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync }: Props) => { +export const LayoutSettingsContent = ({ + board, + form, + isSaving, + saveSettingsAsync, + selectedLayoutId, + setSelectedLayoutId, + apps, +}: Props) => { const tBoard = useI18n("board"); const tLayout = useI18n("layout"); const tCommon = useI18n("common"); @@ -59,18 +73,6 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync }); }, }); - const appIds = useMemo( - () => - Array.from( - new Set( - board.items.flatMap((item) => - item.kind === "app" && typeof item.options.appId === "string" ? [item.options.appId] : [], - ), - ), - ), - [board.items], - ); - const { data: apps = [] } = clientApi.app.byIds.useQuery(appIds, { enabled: appIds.length > 0 }); const openLayoutEditorAsync = async (layout: FormValues["layouts"][number]) => { setEditingLayoutId(layout.id); @@ -107,54 +109,13 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync const displayedLayouts = form.values.layouts .map((layout, index) => ({ layout, index })) .toSorted((first, second) => layoutRoleOrder[first.layout.role] - layoutRoleOrder[second.layout.role]); - const fixedItemPreviewSize = - Math.min(BOARD_FIXED_ITEM_SIZE_MAX, Math.max(BOARD_FIXED_ITEM_SIZE_MIN, form.values.fixedItemSize)) / 4; + const activeLayoutId = + displayedLayouts.find(({ layout }) => layout.id === selectedLayoutId)?.layout.id ?? baseLayout?.id; return ( - - - {tBoard("setting.section.layout.scaling.title")} - - - - - - - {tBoard("field.fixedItemSize.previewValue", { size: form.values.fixedItemSize })} - - - - - + @@ -179,144 +140,169 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync let insertionIndex = form.values.layouts.findIndex((layout) => layout.breakpoint > nextBreakpoint); if (insertionIndex === -1) insertionIndex = form.values.layouts.length; form.insertListItem("layouts", newLayout, insertionIndex); + setSelectedLayoutId(newLayout.id); }} > {tBoard("setting.section.layout.responsive.action.add")} - {displayedLayouts.map(({ layout, index }) => { - const persistedLayout = board.layouts.find((candidate) => candidate.id === layout.id); - const sourceLayout = persistedLayout ?? board.layouts.find((candidate) => candidate.role === "base"); +