Skip to content

feat(Board): fixed cols × rows matrix, exported placement helpers, hover-ring opt-out - #1348

Merged
tenphi merged 1 commit into
mainfrom
feat/board-fixed-matrix
Aug 24, 2026
Merged

feat(Board): fixed cols × rows matrix, exported placement helpers, hover-ring opt-out#1348
tenphi merged 1 commit into
mainfrom
feat/board-fixed-matrix

Conversation

@tenphi

@tenphi tenphi commented Aug 24, 2026

Copy link
Copy Markdown
Member

Three additions to Board, all driven by what Cube Cloud's Grid container widget (cubedevinc/cubejs-enterprise#13919) had to hand-roll around the current API.

A fixed cols × rows matrix

  • rows renders exactly that many rows whatever the content needs — a matrix rather than a grid that hugs its widgets — and implies a maxRows of the same value, so gridBounds keeps every drag and resize inside it without a per-widget maxH.
  • rowHeight="stretch" divides the board's own measured height into those rows.

Together they express "a grid of a declared size that fills the box it is given": the board takes its parent's height and each cell an equal share, so resizing the container resizes the cells instead of adding or removing them.

An app wanting that previously had to measure the box itself, divide by the row count, and feed the result back in as rowHeight — which meant a plain div alongside the board to hang a ResizeObserver on, and rendering nothing at all until the first measurement landed. A stretch board takes height: 100% rather than a min-height derived from its own rows; a self-derived height would be a fixed point (it would measure back exactly what it just asked for) and the cells could never track the container. That is the bug the browser test below caught in the first attempt.

Falls back to the default pixel height when it lacks either a bounded row count or a parent with a height, rather than collapsing.

Two exported placement helpers

placeInFreeSlot and distributeEvenly now ship from grid-core and the package root.

placeInFreeSlot is the rule the board already applies when a dragged widget cannot stay where it was dropped. It was private, so an app adding a widget programmatically — a toolbar drop, a cross-board move made by code — had to re-implement the scan, and the same drop landed in two different cells depending on whether a pointer or a code path served it.

distributeEvenly tiles items across both axes while keeping the row structure the layout already has (grouped into visual rows by y, rows divided across the groups and cols across each group's members). It is the one operation that deliberately grows items — every gesture-driven path refuses to, because growing on a move is a surprise — which is why it is a call the app makes rather than a mode the board is in.

hoverRing

Turns off the resting ring a widget shows on hover, per widget or for the whole board via widgetProps. The ring is the affordance that says a widget can be picked up; on scenery — a chromeless layout container, a spacer — it advertises an interaction the widget does not offer.

Turning it off used to mean overriding styles.shadow, which replaces the whole modifier map, so the selected ring and the drag lift had to be re-authored by hand just to drop one state.

Verification

  • 2107 unit tests + 1 skipped pass, including 14 new ones for the placement helpers.
  • 35 browser tests pass, including three new ones for the matrix — cell height, cells tracking a resized container, and the board painting every declared row. This geometry cannot be asserted in jsdom, which is why they are browser tests.
  • Docs, a FixedMatrix story, and a changeset are included; the defaults registry is regenerated for hoverRing.

🤖 Generated with Claude Code


Note

Medium Risk
Touches Board layout geometry (height measurement, stretch sizing, implied maxRows) and drag/resize bounds. Behavior is additive and covered by unit plus browser tests, but mis-measurement could still affect nested dashboards.

Overview
Board can now be a declared cols × rows matrix that fills its parent instead of hugging content.

rows paints that many rows regardless of widgets and implies maxRows, so drags and resizes stay inside the matrix. rowHeight="stretch" (only with rows) splits the board’s measured height across those rows so cells grow with the container; without rows or a measured height it keeps the default content-hugging pixel height. Stretch boards use height: 100% so they do not lock to a self-derived min-height.

placeInFreeSlot (the existing drop-scan, now public) and distributeEvenly (even tiling that may grow items, keeping visual rows) are exported from the package so programmatic adds match drag placement.

hoverRing (per widget or via widgetProps) turns off the resting hover ring without replacing the whole shadow map. Docs, a FixedMatrix story, and tests cover the new geometry and helpers.

Reviewed by Cursor Bugbot for commit 1e75548. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cube-ui-kit Ready Ready Preview Aug 24, 2026 3:46pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e75548

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cube-dev/ui-kit Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-ca2791e.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 498.45 KB (+0.1% 🔺) Yes 🎉
Tree shaking (just a Button) 121.13 KB (0% 🟰) Yes 🎉

Compared against main at 3e5e3ferun 32739310694, 2026-08-24T14:32:16Z.

To see which modules changed, download the size-limit-statoscope-report artifact from this run and open report.html.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

Comment thread src/components/layout/Board/Board.tsx Outdated
…ver-ring opt-out

`rows` renders exactly that many rows whatever the content needs, and
implies a `maxRows` of the same value so `gridBounds` keeps every drag and
resize inside the matrix. `rowHeight="stretch"` divides the board's own
measured height into those rows. Together they express "a grid of a
declared size that fills the box it is given": the board takes its
parent's height and each cell an equal share, so resizing the container
resizes the cells instead of adding or removing them.

An app wanting that previously had to measure the box itself, divide by
the row count and feed the result back in as `rowHeight` -- which meant
rendering nothing until the first measurement landed, and a plain div
alongside the board to hang the ResizeObserver on.

`placeInFreeSlot` and `distributeEvenly` are now exported from grid-core.
The first is the rule the board already applies when a dragged widget
cannot stay where it was dropped; exporting it means a widget an app adds
programmatically lands where a drag would have put it, rather than
wherever the app's own re-implementation of the scan decided. The second
tiles items evenly across both axes while keeping the row structure the
layout already has, and is the one operation that deliberately grows
items -- which is why it is a call the app makes, not a mode the board is in.

`hoverRing` turns off the resting ring a widget shows on hover. It is the
affordance that says a widget can be picked up, and on scenery -- a
chromeless layout container, a spacer -- it advertises an interaction the
widget does not offer. Turning it off used to mean overriding
`styles.shadow`, which replaces the whole modifier map, so the `selected`
ring and the drag lift had to be re-authored by hand to drop one state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1e75548. Configure here.

? undefined
: isStretching
? { height: '100%' }
: { minHeight: `${containerHeight}px` }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stretch board collapses without parent height

Medium Severity

When rows and rowHeight="stretch" are set, the board always applies height: 100% via isStretching, even if the parent has no definite height. Percentage height then resolves to auto, absolutely positioned widgets contribute no flow size, and with min-height: 0 the board collapses to empty. Row-height math falls back to DEFAULT_ROW_HEIGHT when measuredHeight is 0, but that never restores a content-hugging minHeight, so the documented non-collapsing fallback does not work.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1e75548. Configure here.

@tenphi
tenphi merged commit e0ccb9b into main Aug 24, 2026
16 checks passed
@tenphi
tenphi deleted the feat/board-fixed-matrix branch August 24, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant