From e3f679237ddb26b18892b2620b45a655d3eaaf57 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 2 Aug 2026 09:36:33 +0200 Subject: [PATCH] fix: polish board grid interactions --- apps/docs/docs/management/boards/index.mdx | 9 +- .../boards/(content)/_client.module.css | 2 + .../app/[locale]/boards/(content)/_client.tsx | 8 +- .../board/items/item-content.module.css | 5 +- .../components/board/items/item-content.tsx | 7 +- .../src/components/board/items/item-menu.tsx | 14 +- .../items/widget-hover-overlay.module.css | 7 +- .../board/items/widget-hover-overlay.tsx | 2 +- .../src/components/board/layout/constants.ts | 2 +- .../src/components/board/layout/geometry.ts | 2 +- .../src/components/board/layout/reflow.ts | 39 ++++-- .../board/layout/test/geometry.spec.ts | 1 + .../board/layout/test/reflow.spec.ts | 4 +- .../board/sections/container-section.tsx | 76 +++++++--- .../sections/container/container-menu.tsx | 5 +- .../sections/grid/dnd/coordinates.spec.ts | 5 +- .../board/sections/grid/dnd/coordinates.ts | 8 +- .../board/sections/grid/dnd/engine.spec.ts | 20 ++- .../board/sections/grid/dnd/engine.ts | 15 +- .../board/sections/grid/dnd/index.ts | 2 + .../board/sections/grid/dnd/targeting.spec.ts | 68 +++++++++ .../board/sections/grid/dnd/targeting.ts | 90 ++++++++++++ .../board/sections/grid/fixed-grid-item.tsx | 3 - .../board/sections/grid/grid-editor.css | 28 ++-- .../board/sections/grid/grid-editor.tsx | 125 ++++++++++------- .../sections/grid/section-grid.module.css | 43 +----- .../board/sections/grid/section-grid.tsx | 4 +- .../grid/test/fixed-grid-item.spec.ts | 8 +- .../components/board/sections/item.module.css | 6 + e2e/board-grid.spec.ts | 132 ++++++++++++++++-- 30 files changed, 541 insertions(+), 199 deletions(-) create mode 100644 apps/nextjs/src/components/board/sections/grid/dnd/targeting.spec.ts create mode 100644 apps/nextjs/src/components/board/sections/grid/dnd/targeting.ts diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 2e8137a20..64e6dba54 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -106,7 +106,8 @@ When adding a new layout, its initial positions and sizes are generated from the New items use their widget's default grid size, while a new Container starts at a useful `4 × 3` size when the layout has enough columns. Both are placed in the first available position. -Moving content downward in edit mode extends the canvas automatically, while view mode trims it back to the content. +The current board edge is a hard drag boundary, so moving a tile outside the canvas cancels that placement instead of creating empty rows. +Adding content can still create the rows required for the new tile. Nested Containers keep their own resizable bounds. #### Name @@ -143,14 +144,14 @@ Once the minimum of the next layout is reached, the layout will change. View mode renders a static board without executing the drag-and-drop editor. After the board is interactive, Homarr prepares the editor in the background for users who can modify it. Entering edit mode keeps the static board visible until that preparation finishes, then enables collision-aware movement and resizing without changing the board geometry. -Widget content is inert in edit mode, so dragging anywhere on the content moves the tile instead of activating the widget; the visible grip and tile outline indicate the active move surface, while the tile's settings button remains available. +Widget content is inert in edit mode, so dragging anywhere on the content moves the tile instead of activating the widget; its settings button remains visible in the compact header above the tile. Dropping a tile into another tile's exact slot swaps their positions when both resulting placements fit without overlapping other content. For other collisions, or when a tile grows into occupied space, nearby tiles are compacted out of the way. The visible corner controls provide a 44-pixel resize target, and edge controls appear on hover for horizontal or vertical resizing. Resize handles stop at the tile's minimum allowed size while the pointer is still moving. Exit edit mode to save the resulting positions. -Pointer users can drag anywhere on a tile; the grip is a visual cue for that move surface rather than a separate control. +Pointer users can drag anywhere on a widget tile. Containers use the grab button at their top-left corner. Resize tiles from any edge or corner handle. On a touch screen, press and hold the tile before moving it; resize handles support direct touch input. Keyboard users can focus a tile, press Enter or Space to start editing, use the arrow keys to move it, and use Shift plus the arrow keys to resize it. @@ -179,7 +180,7 @@ Each Container can configure: - Whether its menu shows **Open all in new tabs** for the app links it contains. - Its border color and custom CSS classes. -Collapsing a Container compacts the surrounding layout without losing its expanded size. +Collapsing a Container reduces it to a half-row expansion pill and compacts the surrounding layout without losing its expanded size. Expanding it restores those bounds and moves following items out of the way. ### Background 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 fe7d183d2..3dbf87f39 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css @@ -12,6 +12,8 @@ position: sticky; top: 0; align-self: start; + padding-top: 24px; + margin-top: -24px; max-height: calc(100dvh - var(--app-shell-header-offset, 0px) - var(--mantine-spacing-md)); overflow-y: auto; overscroll-behavior: contain; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx index 01f94bd68..c710ea24c 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx @@ -98,7 +98,7 @@ export const ClientBoard = () => { .map(getLogicalTrackSize); const logicalWidth = laneWidths.reduce((total, width) => total + width, 0) + (laneWidths.length - 1) * LOGICAL_GRID_GAP; - const initialLogicalHeight = getInitialBoardLogicalHeight(board, currentLayoutId); + const initialLogicalHeight = getInitialBoardLogicalHeight(board, currentLayoutId) + LOGICAL_GRID_GAP; const initialAvailableWidth = Math.max(1, initialViewportWidth - APP_SHELL_INLINE_PADDING); const gridTemplateColumns = [ leftColumnCount > 0 ? `${getLogicalTrackSize(leftColumnCount)}px` : null, @@ -121,7 +121,11 @@ export const ClientBoard = () => { > -
+
{leftColumnCount > 0 && leftSection && (