From 6f2bcb8054fc4a7d60402480fb25b3c875a6d19f Mon Sep 17 00:00:00 2001 From: ajnart Date: Tue, 8 Sep 2026 23:58:16 +0200 Subject: [PATCH] fix(board): restore container grid geometry --- .../board/sections/grid/grid-editor.css | 4 +- .../sections/grid/section-grid.module.css | 5 ++ .../board/sections/grid/section-grid.tsx | 47 ++++--------------- .../components/board/sections/item.module.css | 3 ++ 4 files changed, 19 insertions(+), 40 deletions(-) diff --git a/apps/nextjs/src/components/board/sections/grid/grid-editor.css b/apps/nextjs/src/components/board/sections/grid/grid-editor.css index 4b7e47054..fc71774f7 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-editor.css +++ b/apps/nextjs/src/components/board/sections/grid/grid-editor.css @@ -1,9 +1,11 @@ .board-grid-editor { position: absolute; - inset: 0 auto auto 0; + inset: 0 0 auto; + margin: 0 auto; overflow: visible; border-radius: var(--mantine-radius-md); pointer-events: none; + zoom: var(--board-grid-content-scale, 1); } .board-grid-entry { diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css index df8a2c3ea..88a0f3968 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css @@ -63,6 +63,11 @@ overflow: visible; } +/* Containers provide their own inner item gap, so their card must keep the full grid aspect ratio. */ +.staticItem[data-type="section"] > .contentMount { + inset: 0; +} + .staticItem[data-type="item"] > .contentMount { inset: calc(10px * var(--board-canvas-inverse-scale, 1)); } diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx index d606e30db..b1a04a56d 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx @@ -143,40 +143,15 @@ export const SectionGrid = ({ const isScrollableContainer = section.kind === "container" && section.options.scrollable; const viewportRowCount = viewportRowCountOverride ?? (isScrollableContainer ? Math.max(requestedRowCount, 1) : rowCount); - // A container's own visible card is inset from its allocated board cell by the board's - // standard per-item gap (see the base, non-container `.staticItem[data-type="item"] > - // .contentMount` rule in section-grid.module.css - a container is placed as an "item" like - // any widget, so it gets that same gap). A widget's content just fills whatever size its - // card ends up being, but this SectionGrid instead computes its own fixed pixel size from - // the same column/row counts used to allocate the *outer*, uninset cell - so without - // subtracting that gap back out here, a container's inner grid renders larger than its own - // card and visually spills past its right/bottom edges. 10 must match that CSS rule's inset. const effectiveCanvasScale = Number.isFinite(canvasScale) && canvasScale > 0 ? canvasScale : 1; - const outerCardInset = section.kind === "container" ? (2 * 10) / effectiveCanvasScale : 0; - // A collapsible container's toggle bar (see the `containerToggle` Button in - // container-section.tsx) is an absolutely positioned overlay sitting on top of this grid. - // Its height comes from a Mantine size prop, which - like spacing/font-size - is compensated - // by --mantine-scale to render at a constant physical size regardless of board zoom (see - // scaled-board-canvas.module.css). That makes it behave like the outerCardInset gap above, - // not like the grid's own un-compensated logical-pixel math, so it needs the same - // effectiveCanvasScale conversion - a plain, unconverted subtraction would under-reserve on - // a zoomed-out board and leave the header overlapping the content. Without reserving it at - // all, the header bar covers the first row of the container's content instead of sitting - // above it. Only the vertical axis is affected - the toggle spans the full width already. + // The collapsible label keeps a fixed physical height while the grid uses logical canvas pixels. + // Convert that height so the label reserves the same space at every board scale. const collapsibleHeaderInset = section.kind === "container" && section.options.collapsible ? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale : 0; - const logicalWidth = getLogicalGridSize(columnCount) - outerCardInset; - const viewportHeight = Math.max(1, getLogicalGridSize(viewportRowCount) - outerCardInset - collapsibleHeaderInset); - // Items are positioned in a coordinate space sized to the *un-inset* column/row count - // (fullGridWidth/Height below - see getLogicalItemStyle), but logicalWidth/Height above are - // deliberately smaller by outerCardInset to match the container's actual visible card size. - // Left alone, that mismatch means the items - not just the grid's own box - spill past the - // card's right/bottom edge by exactly that inset. Scale the grid's content down by the same - // ratio the board's own canvas uses for its whole-board zoom (see ScaledBoardCanvas), just one - // level deeper, so it fits the actual card instead of clipping. --board-canvas-ui-scale is - // corrected to compensate so icon/text sizing inside the container isn't affected. const fullGridWidth = getLogicalGridSize(columnCount); const fullGridHeight = getLogicalGridSize(rowCount); + const logicalWidth = fullGridWidth; + const viewportHeight = Math.max(1, getLogicalGridSize(viewportRowCount) - collapsibleHeaderInset); // For a scrollable container, rowCount covers *all* content rows, not just the visible card - // fullGridHeight grows right along with it, so a ratio built from it drifts toward 1 as content // grows regardless of the (fixed) inset, under-scaling relative to what the actually-visible @@ -184,16 +159,9 @@ export const SectionGrid = ({ // describe the real visible card size in both cases (they equal the non-scrollable values when // rowCount and viewportRowCount are the same), so use those instead. const fullViewportHeight = getLogicalGridSize(viewportRowCount); - // A uniform zoom is required (not a non-uniform transform scale(x, y)) - --board-canvas-ui-scale - // is a single scalar that every icon/text/custom-CSS size compensation in the app multiplies - // by, so a non-uniform stretch can never be captured by it correctly. That means one axis can - // be left with a small amount of unfilled slack when width/height need different ratios to - // exactly fit - centered below so it's even on both sides rather than left-aligned. - // logicalWidth/viewportHeight can go non-positive for a narrow (e.g. single-column) container - // on a heavily zoomed-out board, where outerCardInset (which grows as canvasScale shrinks) can - // exceed the container's own un-inset size. Floor the scale well above 0 so it can never reach - // zero or negative - combinedUiScale divides by this value, and this value is also used as - // zoom directly, both of which break (Infinity/NaN, or invalid/collapsed content) otherwise. + // A collapsible container reserves a fixed-height label above the square-cell grid. Keep the + // nested content uniformly scaled within the remaining height so labels and icons retain their + // proportions; ordinary containers use the original unscaled, edge-aligned grid geometry. const containerContentScale = section.kind === "container" && fullGridWidth > 0 && fullViewportHeight > 0 ? Math.max(0.01, Math.min(logicalWidth / fullGridWidth, viewportHeight / fullViewportHeight, 1)) @@ -302,6 +270,7 @@ export const SectionGrid = ({ height: `var(--board-grid-drag-height, ${viewportHeight}px)`, marginTop: collapsibleHeaderInset || undefined, "--board-item-radius": `var(--mantine-radius-${board.itemRadius})`, + "--board-grid-content-scale": containerContentScale, } as CSSProperties } data-section-id={section.id} diff --git a/apps/nextjs/src/components/board/sections/item.module.css b/apps/nextjs/src/components/board/sections/item.module.css index e8b9e4d8f..8fac711ce 100644 --- a/apps/nextjs/src/components/board/sections/item.module.css +++ b/apps/nextjs/src/components/board/sections/item.module.css @@ -21,6 +21,9 @@ --border-color: rgb( from var(--container-border-color, var(--container-default-border-color)) r g b / var(--opacity, 1) ); + /* Keep the border visual without letting it shrink the square-cell grid's content box. */ + border-width: 0; + box-shadow: inset 0 0 0 calc(1px * var(--board-canvas-inverse-scale, 1)) var(--border-color); } .itemCard:global(.iframe-wrapper) {