Widget name is required
1 filter narrowing this widget.
npx shadcn@latest add https://sdk-components.thesqd.com/r/dashboard-widget.jsonimport {
DashboardWidget,
type DashboardWidgetConfig,
} from "@/components/blocks/dashboard-widget";"use client";
import { DashboardWidget } from "@/components/blocks/dashboard-widget";
import type { FilterField } from "@/components/blocks/filter-builder";
const FIELDS: FilterField[] = [
{ id: "title", label: "Task" },
{
id: "status",
label: "Status",
options: [
{ value: "queued", label: "Queued" },
{ value: "delivered", label: "Delivered" },
],
},
];
const TASKS = [
{ id: "1", title: "Stewardship campaign brief", status: "queued" },
{ id: "2", title: "Annual report cover", status: "delivered" },
];
export function Example() {
return (
<DashboardWidget
records={TASKS}
fields={FIELDS}
defaultConfig={{ name: "My queue", content: "list" }}
/>
);
}
DashboardWidget — one card, two faces
.t-flip (perspective box, height measured from the showing face, .t-resize tween)
└── .t-flip-inner (rotateY, data-flipped)
├── FRONT .t-flip-face
│ ├── header — widget name · count pill · gear button → flips
│ └── content — the records that pass `config.filters`
│ ├── content: "list" → ItemList (listColumns)
│ ├── content: "table" → DataGridV3 (tableColumns, read-only, no toolbar)
│ └── emptyState when nothing passes
└── BACK .t-flip-face.t-flip-back
├── header — back arrow · "Configure" · matched / total
├── TextField — the widget's custom name
├── Select — List / Table (only offered when tableColumns is given)
├── FilterBuilder inline — the shared nested AND/OR builder
└── footer — Done → flips back
Config — the whole of a widget's state, one object to persist
DashboardWidgetConfig { name, content: "list" | "table", filters: FilterGroup }
Data flow
├── The widget never owns the records. It holds a FilterGroup and evaluates it
│ with `filterRecords(records, config.filters, fields)`, the same call a
│ board or a table makes — so a saved filter reads the same everywhere.
└── `config` / `onConfigChange` controlled, or `defaultConfig` uncontrolled.
`flipped` / `onFlippedChange` does the same for the face, so a dashboard
can put every widget into config mode at once.
Motion
├── .t-flip — rotateY(180deg) over --flip-dur / --flip-ease, backface hidden
├── .t-resize — the two faces rarely match height, so the box tweens between
│ them on the same curve rather than snapping
└── The hidden face is visibility:hidden, so it is out of the tab order and
out of the accessibility tree — not just turned away from the viewer.Widget name is required
1 filter narrowing this widget.
Task | Status | Due |
|---|---|---|
| Stewardship campaign brief | Queued | Mar 4 |
| Easter series titles | In progress | Mar 7 |
| Baptism invite carousel | Queued | Mar 11 |
Widget name is required
1 filter narrowing this widget.
DashboardWidget
| Prop | Type | Default | Description |
|---|---|---|---|
records | Record<string, unknown>[] | — | The records the widget draws. Each needs a stable `id`. Passed to the filter builder too, so fields with no `options` offer the distinct values present here. |
fields | FilterField[] | — | Fields the config's filter builder can test. |
listColumns | ItemListColumn[] | — | Columns for `content: "list"`. Omitted, ItemList infers one per non-reserved key. |
tableColumns | DataGridV3Column[] | — | Columns for `content: "table"`. Without it the table content is not offered at all, so the config never presents a mode the widget can't draw. |
config | DashboardWidgetConfig | — | Controlled config — name, content and filters. Pair with `onConfigChange`. |
defaultConfig | Partial<DashboardWidgetConfig> | — | Uncontrolled starting config. `filters` accepts a flat clause list as well as a full group. |
onConfigChange | (next: DashboardWidgetConfig) => void | — | Fired with the whole config after any edit on the back face. |
contentOptions | ("list" | "table")[] | both | Content types the config offers. A single entry hides the Show as select entirely. |
flipped | boolean | — | Controlled face — `true` shows the config. Pair with `onFlippedChange`. |
defaultFlipped | boolean | false | Start on the config side. |
onFlippedChange | (flipped: boolean) => void | — | Fired when the gear, the back arrow or Done turns the card. |
contentHeight | number | 260 | Height of the content area under the header, on both faces. |
emptyState | ReactNode | — | Rendered in place of the content when nothing passes the filters. |
className | string | — | Applied to the flip box. |
DashboardWidgetConfig
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | "Untitled widget" | The widget's custom name, shown in the front header and edited on the back. |
content | "list" | "table" | "list" | How the front face draws its rows. Falls back to the first offered content when the stored one isn't available. |
filters | FilterGroup | — | The nested AND/OR tree the widget evaluates over `records`. An empty group shows everything. |