Nasaq

DashboardBoard

A customisable dashboard grid where people drag to reorder cards, resize them, pin favourites, add or remove widgets and change each widget's settings, with an edit mode that saves or cancels as one change.

PreviewOpen ↗

Code

import { DashboardBoard, StatCard, type BoardItem, type DashboardWidgetDef } from "@fadymondy/nasaq/web";import { useState } from "react";const widgets: DashboardWidgetDef[] = [  { type: "revenue", title: "Revenue", unique: true, render: () => <StatCard label="This month" value={412500} /> },  { type: "orders", title: "Orders", render: () => <StatCard label="Today" value={186} /> },];export function Home({ initial }: { initial: BoardItem[] }) {  const [layout, setLayout] = useState(initial);  return (    <DashboardBoard      title="Overview"      widgets={widgets}      layout={layout}      onSave={async (next) => {        await api.saveDashboard(next);        setLayout(next);      }}    />  );}

Layout · beta

Live examples and controls: DashboardBoard in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/dashboard-board.json

A grid of widget cards the user can rearrange. Your app defines the widgets (what each one renders and which settings it has); the board owns the layout, the edit mode and the saving. Drag and resize use @dnd-kit.

When to use

  • A home or overview screen where different roles care about different numbers.
  • Any page of independent cards that should remember how a person arranged them.

When not to use

  • A fixed page layout: use plain StatGrid and cards.
  • Reordering one flat list: use Repeater.

Import

import { DashboardBoard, type BoardItem, type DashboardWidgetDef } from "@fadymondy/nasaq/web";

Quick start

import { DashboardBoard, StatCard, type BoardItem, type DashboardWidgetDef } from "@fadymondy/nasaq/web";
import { useState } from "react";

const widgets: DashboardWidgetDef[] = [
  { type: "revenue", title: "Revenue", unique: true, render: () => <StatCard label="This month" value={412500} /> },
  { type: "orders", title: "Orders", render: () => <StatCard label="Today" value={186} /> },
];

export function Home({ initial }: { initial: BoardItem[] }) {
  const [layout, setLayout] = useState(initial);
  return (
    <DashboardBoard
      title="Overview"
      widgets={widgets}
      layout={layout}
      onSave={async (next) => {
        await api.saveDashboard(next);
        setLayout(next);
      }}
    />
  );
}

Anatomy

DashboardBoard       data-slot="dashboard-board"
├─ toolbar           Customise, or in edit mode: Add widget, Reset, Cancel, Save
├─ grid (ul)         4, 2 or 1 columns by the board's own width
│  └─ card (li)      data-slot="dashboard-board-card"
│     ├─ header      drag handle, title, pin, size, actions "..."
│     ├─ content     your widget (inert while editing)
│     └─ resize grip bottom inline-end corner (pointer only)
├─ add dialog        the widget catalogue
└─ settings dialog   generated from the widget's fields

API

PropTypeDefaultDescription
widgetsDashboardWidgetDef[]requiredThe widget catalogue.
layoutBoardItem[]requiredThe saved layout. Unknown types and repeated ids are dropped, spans are clamped, pinned cards come first.
onSave(layout) => void | Promise<void>requiredCalled by Save and by a settings change made outside edit mode. Reject to keep the editor open with an error. Update layout when it resolves.
defaultLayoutBoardItem[]noneWhat Reset to default returns to. Without it there is no Reset.
editing defaultEditing onEditingChangebooleanfalseEdit mode.
rowHeightnumber200Pixel height of one grid row.
titleReactNodenoneHeading above the toolbar.
loading error onRetryStates.
labelsPartial<DashboardBoardLabels>en / arText overrides.

BoardItem

{ id, type, cols, rows, pinned?, settings? }. id is unique on the board (you can have two "chart" widgets: chart, chart-2); type names the widget.

DashboardWidgetDef

FieldDescription
type, title, description?Identity and catalogue text.
render(ctx)Returns the content. ctx is { id, settings, cols, rows, editing }; cols is what fits now, so a widget can show less on a small card.
minCols maxCols minRows maxRowsSize limits, default 1 to 4.
defaultCols defaultRowsSize when added.
uniqueOnly one on the board.
fieldsSettings: select, number, toggle or text, each with key and label. No fields means no Settings action.
defaultSettingsValues used until the user changes them.

Examples

A widget with a setting that changes what it shows:

{
  type: "volume",
  title: "Ticket volume",
  minCols: 2,
  fields: [{ key: "days", label: "Period", type: "select", options: [{ value: "7", label: "7 days" }, { value: "30", label: "30 days" }] }],
  defaultSettings: { days: "7" },
  render: ({ settings }) => <VolumeChart days={Number(settings.days)} />,
}

Accessibility

Every pointer gesture has a keyboard or menu equivalent. Focus a card's drag handle, press Space, move with the arrow keys and press Space again; each step is announced ("Orders is now at position 3 of 7"). The "..." button, context-click, long press, Shift+F10 and the Menu key open the same menu: wider, narrower, taller, shorter, move earlier, move later, pin, settings, remove. Size changes, pins, adds and removes are announced. The resize grip is a pointer shortcut only and is hidden from assistive technology. Widget content is inert in edit mode so it cannot be tabbed into while the layout moves. Save errors use role="alert".

RTL & i18n

The grid, the drag order and the resize grip follow the reading direction: in Arabic the grip sits at the left and dragging it leftwards widens the card. Sizes are shown as "columns×rows" in Western digits. English and Arabic text ship built in; override with labels. Widget titles and settings labels come from you.

Styling & tokens

Cards are Card; the editing outline uses border and ring tokens. Target [data-slot="dashboard-board"] and [data-slot="dashboard-board-card"]; extend with className.

Do / Don't

  • Do give widgets sensible minCols, so a chart is never squeezed to one column.
  • Do keep ids stable; they are what a saved layout refers to.
  • Do update layout only after onSave resolves.
  • Don't rely on the resize grip alone; the menu is the accessible path.
  • Don't put a widget's own drag handlers inside a card; content is inert while editing.
  • Widths only change on a four column board. On two or one columns a card keeps its saved width and only its height changes.

Lab

https://docs.nasaqui.com/?path=/docs/components-layout-pages-dashboard-board--docs

On this page