Widget boards: a grid engine, tiles whose figures reconcile with the data under them, and a layout the reader can rearrange, hide and reset.
Page-class laws·docs/dashboard.md·105 lines
Kit src/components/gridstack/Living example · Dashboard
How every dashboard / board page in this repo is built. The kit is DashboardBoard (+ GridBoard / useGridBoard / DashboardToolbar / StatTile underneath it) in src/components/gridstack/; the living examples are the reference board (/labs/layout/dashboard), the CRM dashboard (/labs/apps/crm) and the twelve Industries screens. Same contract as data-view.md: the kit enforces what it can with defaults, this file records the rest.
01
A dashboard is a composition of independent features, so the one-feature-one-card law applies with full force: every widget is exactly one standard Card, and the board itself owns no chrome at all — cells are transparent, spacing comes from the grid margin, and the page never writes grid CSS or GridStack init code. Everything a user can do to a widget (drag, resize, show/hide) belongs to the board layer; everything inside the card belongs to the widget's own feature.
02
const WIDGETS: GridBoardWidget[] = [
{ id: "revenue", label: "Revenue", w: 6, h: 3, minW: 4, minH: 2, node: <StatRevenueCard /> },
…
];
<DashboardBoard title="Grid control room" icon="icon-[solar--bolt-bold-duotone]" widgets={WIDGETS} />That is the whole page. A dashboard writes a widget list and nothing else — no visibility reducer, no re-key expression, no grid options.
| Piece | Component | Rule |
|---|---|---|
| Board | DashboardBoard | Toolbar + grid + the visible-set state that binds them. It owns the two things pages used to re-derive by hand: the state lives outside the toolbar (so Customize can't disagree with what renders) and the grid re-keys on the visible ids (so React never removes items from under GridStack). |
| Toolbar | DashboardToolbar | One card-surfaced bar: identity tile + "N of M widgets shown" readout left, the Customize popover right. Presentation only; the list scrolls past ~11 widgets so Reset stays reachable. |
| Grid | GridBoard | Declarative widget list; house defaults are the law: 12 columns, cellHeight 80, margin 12, whole-card drag, SE-corner resize only, 1 column below md. |
| Widget | GridBoardWidget | id (stable, never reused), label (what Customize shows), w/h tuned so nothing clips at the default size, minW/minH guarding the widget's readable floor, node = one card. |
| KPI widget | StatTile | The shared headline tile — mono label, one figure, one mono caption, on a standard Card. Screens derive its figures from the same modules their cards consume. Don't hand-roll a fifth one. |
03
rounded-3xl surface — that IS their card — and take fill to drop the standalone max-width and stretch to the cell.StatTile widgets a user can hide one at a time, not one card containing a grid.PageHeader and the ComposedOf traceability footnote.GRID_DRAG_CANCEL list already covers links, buttons, form controls, menus and recharts; add data-no-drag (or the .no-drag class) for anything exotic instead of touching the drag options.w/h fit the widget's natural content at cellHeight 80 so nothing clips before the user ever resizes; minW/minH stop a resize from making the widget unreadable.title / icon to the toolbar; the bar's shape never changes. A board-level control (Refresh, Export) goes in the toolbar's actions slot, left of Customize — never in a second bar.md the board is a plain stack. The kit's CSS hands positions and heights back to normal flow there, because a card is always taller at 350px than at 600px and a height tuned wide clips its own content narrow. Don't fight it with per-page breakpoints.04
<Card> feature (mock data deterministic, per the house rules).WIDGETS list: stable ids, human labels, content-derived w/h + minW/minH.<DashboardBoard widgets={WIDGETS} title=… icon=… /> — that is the entire page body; there is nothing else to write.nav-data.ts and add/extend the feature-ledger entry.05
Card, or a Card wrapped in extra cell chrome.id for a different widget (Customize state keys on it).