Skip to content

feat(Board): add a dashboard board of movable, resizable items; prepare 0.2.0-alpha.18 - #63

Merged
yomybaby merged 5 commits into
mainfrom
feat/board
Oct 10, 2026
Merged

yomybaby merged 5 commits into
mainfrom
feat/board

Conversation

@yomybaby

Copy link
Copy Markdown
Member

Summary

Adds Board, a dashboard grid of items the user moves and resizes, and prepares 0.2.0-alpha.18.

It stands in for @cloudscape-design/board-components in the Backend.AI WebUI, which ships it today (its BAIBoard becomes an adapter over this component; the WebUI PR follows this release). The data model is Cloudscape's — spans plus a columnOffset per column count, onItemsChange reporting the whole board in reading order — so a layout a product persisted from that board renders and persists the same here (layout.ts is a faithful port of interpretItems / transformItems).

Interaction follows the replaced board: a handle dragged by pointer moves the item under the pointer, or sizes it live in px, and snaps to the grid on release; a handle clicked, or given Enter / Space, is activated — direction buttons appear and they or the arrow keys step one cell, Enter / Space / leaving the handle commits, Escape discards. How the other items make room is a port of Cloudscape's path-based solution search (engine.ts: scored directions, swaps, refloat, conflicts). A pointer drag follows the pointer through ancestor scrolling and auto-scrolls the nearest scroll container near its edge.

  • No new dependency: pointer and keyboard handling are the component's own; the handle glyphs are inline SVG; a11y strings and live-region announcements are uic.Board.* keys in every locale.
  • Astryx-shaped props (isMovable, isResizable, variant="bordered", renderItem, emptyContent, columnBreakpoints, itemClassName, handle icons / labels, announcement builders). CSS knobs: --board-row-height, --board-gap, --board-item-radius, --board-transition-duration, --board-handle-z.
  • Measured in the WebUI against the Cloudscape board: production bundle −97 KB gzip (−306 KB raw CSS), /dashboard first board item −13% (−483 ms, TBT −55%), login page first render unchanged.

Verification

  • pnpm run verify: tsc, eslint, prettier, boundary, theme:check, 1523 tests, build, check:pack, astryx doctor integration validate 0 errors.
  • Board: 58 unit tests (layout port fixtures, engine, keyboard / pointer / resize paths incl. scroll compensation, direction buttons, focus restoration).
  • Browser: the WebUI dashboards (Dashboard, Admin Dashboard, Start) on a dev server, seven move / resize scenarios instrumented per frame after commit (0 residual motion), persisted layouts from the Cloudscape board rendered unchanged.

Board is ui-common's replacement for @cloudscape-design/board-components:
a grid of items the user moves and resizes by pointer or keyboard, with
every step announced in a live region. It adds no dependency: the drag and
resize handling is the component's own, and the handle glyphs are inline
SVG on currentColor.

The data model is Cloudscape's, so a layout a product persisted from that
board renders and persists the same. An item is its spans plus a
columnOffset per column count; the column count follows the board's width
(1 under 688px, 2 under 912px, 4 under 2100px, else 6). interpretItems and
transformItems are ports of Cloudscape's algorithms: default placement
packs into column heights, columnOffset[columns] is honoured when it fits,
and a commit keeps the offsets stored for other column counts up to the
first index whose order changed (or the resized item) and drops them from
there on.

The engine is simpler than Cloudscape's solver. The active item goes where
the user put it; each item it covers takes the space it vacated when it
fits there without touching anything, else drops to the row below the item
that displaced it, keeping its column, and pushes in turn; then everything
but the active item floats up. Each preview is computed from the layout
the operation started with, so it depends on the target, not the path.

A pointer drag reads the pointer in viewport coordinates and adds back how
far the grid's origin travelled since the drag started, so an ancestor
scrolling under a still pointer (the product's content area is the scroll
container, not the window) moves neither the target nor the dragged item
off the pointer; a window-level scroll listener re-applies the last pointer
position. While the pointer is held within 48px of an edge of the nearest
scroll container (computed overflow auto or scroll; else the document's
scrolling element), a requestAnimationFrame loop scrolls it, faster the
closer to the edge, so a long board can be dragged end to end.

Prepare 0.2.0-alpha.18 (library and CLI).
Resize by pointer sizes the item in px as the pointer moves and snaps to the grid on release. A handle clicked, or given Enter or Space, is activated: direction buttons appear around it and they or the arrow keys step one cell at a time; Enter, Space or leaving the handle commits, Escape discards. Arrows do nothing on a handle that is not activated. The layout engine is a port of Cloudscape's path-based solution search (scored directions, swaps, refloat, conflicts), so the other items make room the same way. The handles draw no surface on hover or press.
The slide transition applied to every item at all times, so when a move or resize committed and the transforms came off in the same render that re-placed the items, each displaced item was drawn at its new cell plus its old translation and slid back. The transition now exists only while an operation is on, as in the board this one replaces.
…focused

When a commit reorders the items, React moves the active item's node in the DOM. The focused handle inside it loses focus, and the browser recomputes styles for that before the board's own class changes: an item's transform is gone but the ancestor-scoped transition is still there, so the item slides in from where its transform had it. The transition is now the item's own class, which leaves in the same update as the transform. The handle that lost focus to the move is focused again after the render, as the replaced board did.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Direction controls are inaccessible, off-grid drops announce an incorrect result, and release links are incomplete.

3 open findings
What changed in this PR

Adds a movable, resizable dashboard Board compatible with Cloudscape layouts and prepares the alpha.18 release.

Changes:

  • Implements Board layout, interaction, scrolling, styling, and tests.
  • Adds localized accessibility messages and public exports.
  • Updates documentation and package versions.
File Description
src/​index.ts Exports the Board API.
src/​i18n/​catalog.ts Registers Board messages.
src/​i18n/​locales/​zh-TW.json Adds Traditional Chinese strings.
src/​i18n/​locales/​zh-CN.json Adds Simplified Chinese strings.
src/​i18n/​locales/​vi-VN.json Adds Vietnamese strings.
src/​i18n/​locales/​tr-TR.json Adds Turkish strings.
src/​i18n/​locales/​th-TH.json Adds Thai strings.
src/​i18n/​locales/​ru-RU.json Adds Russian strings.
src/​i18n/​locales/​pt-PT.json Adds European Portuguese strings.
src/​i18n/​locales/​pt-BR.json Adds Brazilian Portuguese strings.
src/​i18n/​locales/​pl-PL.json Adds Polish strings.
src/​i18n/​locales/​ms-MY.json Adds Malay strings.
src/​i18n/​locales/​mn-MN.json Adds Mongolian strings.
src/​i18n/​locales/​ko-KR.json Adds Korean strings.
src/​i18n/​locales/​ja-JP.json Adds Japanese strings.
src/​i18n/​locales/​it-IT.json Adds Italian strings.
src/​i18n/​locales/​id-ID.json Adds Indonesian strings.
src/​i18n/​locales/​fr-FR.json Adds French strings.
src/​i18n/​locales/​fi-FI.json Adds Finnish strings.
src/​i18n/​locales/​es-ES.json Adds Spanish strings.
src/​i18n/​locales/​el-GR.json Adds Greek strings.
src/​i18n/​locales/​de-DE.json Adds German strings.
src/​components/​Board/​Board.tsx Implements the Board component and interactions.
src/​components/​Board/​Board.css Styles the grid, handles, and transitions.
src/​components/​Board/​Board.messages.ts Defines accessibility messages.
src/​components/​Board/​Board.test.tsx Tests component interactions and behavior.
src/​components/​Board/​engine.ts Implements layout conflict resolution.
src/​components/​Board/​index.ts Defines Board subpath exports.
src/​components/​Board/​layout.ts Converts persisted and rendered layouts.
src/​components/​Board/​layout.test.ts Tests layout transformations and engine behavior.
src/​components/​Board/​scroll.ts Implements scroll compensation and auto-scroll.
src/​components/​Board/​scroll.test.ts Tests scrolling utilities.
README.md Lists Board among custom components.
CONTRIBUTING.md Documents Board dependencies.
CHANGELOG.md Adds the alpha.18 release notes.
package.json Bumps the library to alpha.18.
packages/​cli/​package.json Bumps the CLI to alpha.18.
exports.customs.json Adds the Board package export.
astryx/​docs/​ui-common.doc.mjs Adds Board to aggregate documentation.
astryx/​components/​Board.tsx Adds the Astryx documentation export shim.
astryx/​components/​Board.doc.mjs Documents the Board API and usage.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/components/Board/Board.tsx Outdated
Comment thread src/components/Board/Board.tsx Outdated
Comment thread CHANGELOG.md
A click that activates a handle focuses it, so Escape, the arrows and leaving the handle end the activation in browsers that do not focus a clicked button. The direction controls are named buttons out of the tab order, so a touch screen reader can find them. A pointer drag released off the grid is announced as discarded. The changelog links the alpha.18 release.
@yomybaby

Copy link
Copy Markdown
Member Author

Ready-gate agent review (Opus) — blocking issues only

# File:line Finding Outcome
O1 Board.tsx:851 (onHandlePointerUp) A click that activates a handle never focuses it, so in browsers that do not focus a clicked button (Safari, Firefox on macOS) the arrows, Escape and blur cannot end the activation: the lattice stays up and the column count stays frozen. Fixed in 120f6bc: the activating click focuses the handle (focus({ preventScroll: true })); the jsdom test asserts the focus and that Escape then discards.

Copilot: reviewed (3 threads, all applied in 120f6bc). Fixes landed after both reviews and were not re-reviewed; pnpm run verify (1524 tests) passes instead.

@yomybaby
yomybaby marked this pull request as ready for review October 10, 2026 00:57
@yomybaby
yomybaby merged commit 48375e0 into main Oct 10, 2026
3 checks passed
@yomybaby
yomybaby deleted the feat/board branch October 10, 2026 01:39
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.

2 participants