Dashboard widget

A dashboard card that turns over to reveal its own settings.

My queue3
Stewardship campaign brief
Queued
Mar 4
Easter series titles
In progress
Mar 7
Baptism invite carousel
Queued
Mar 11
Configure3 of 6

1 filter narrowing this widget.

Where
Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/dashboard-widget.json
Usage
Hand it the records and the fields a filter can test; the widget owns its name, its shape, and its filter tree.
TSImport
import {
  DashboardWidget,
  type DashboardWidgetConfig,
} from "@/components/blocks/dashboard-widget";
TSExample
"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" }}
    />
  );
}
Composition
The two faces, the config object they share, and the motion that turns between them.
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.
List widget
The default face: an `ItemList` of whatever passes the widget's filters, a count pill, and a gear in the corner. The gear turns the card over to the config — rename it, and narrow it with the shared inline `FilterBuilder`, so the filter a designer saves on a board means the same thing in a widget. The card measures the face that's showing and tweens between the two heights, so the turn and the resize read as one motion.
My queue3
Stewardship campaign brief
Queued
Mar 4
Easter series titles
In progress
Mar 7
Baptism invite carousel
Queued
Mar 11
Configure3 of 6

1 filter narrowing this widget.

Where
Table widget
The same widget with `tableColumns`, drawing through data grid v3 instead — read-only, no toolbar, scrolling in place. Pass both column sets and the config offers a **Show as** select, so one widget covers both shapes. This example is controlled: `config` + `onConfigChange` hand you the whole of the widget's state as one object to persist per user.
This week's tasks3
Task
Status
Due
Stewardship campaign briefQueuedMar 4
Easter series titlesIn progressMar 7
Baptism invite carouselQueuedMar 11
3 rows
Configure3 of 6

1 filter narrowing this widget.

Where
API Reference
Props for the widget and the config object its back face edits.

DashboardWidget

PropTypeDefaultDescription
recordsRecord<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.
fieldsFilterField[]—Fields the config's filter builder can test.
listColumnsItemListColumn[]—Columns for `content: "list"`. Omitted, ItemList infers one per non-reserved key.
tableColumnsDataGridV3Column[]—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.
configDashboardWidgetConfig—Controlled config — name, content and filters. Pair with `onConfigChange`.
defaultConfigPartial<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")[]bothContent types the config offers. A single entry hides the Show as select entirely.
flippedboolean—Controlled face — `true` shows the config. Pair with `onFlippedChange`.
defaultFlippedbooleanfalseStart on the config side.
onFlippedChange(flipped: boolean) => void—Fired when the gear, the back arrow or Done turns the card.
contentHeightnumber260Height of the content area under the header, on both faces.
emptyStateReactNode—Rendered in place of the content when nothing passes the filters.
classNamestring—Applied to the flip box.

DashboardWidgetConfig

PropTypeDefaultDescription
namestring"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.
filtersFilterGroup—The nested AND/OR tree the widget evaluates over `records`. An empty group shows everything.