Widgets are the contents of a dashboard section, and of a drawer's widget tab.
Each builder returns a `WidgetConfig`; every `query` runs on the server with a
`WidgetContext`.

```ts excerpt
import { dashboard, metric, statGroup, table } from "@flowpanel/kit";
import { areaChart } from "@flowpanel/kit/charts";

export const overview = dashboard({
  path: "/",
  label: "Overview",
  dateRange: { preset: "last30d" },
  sections: [
    {
      label: "Revenue",
      widgets: [
        metric("MRR", async ({ db, dateRange }) => mrr(db, dateRange), {
          format: "currency",
          span: 3,
          drilldown: "/admin/invoices",
        }),
        areaChart("Revenue over time", async ({ db }) => revenueSeries(db), {
          x: "day",
          y: ["subscriptions", "one-off"],
          stacked: true,
          span: 9,
        }),
      ],
    },
  ],
});
```

## WidgetContext

What every widget `query` receives.

**WidgetContext** — Server-side context passed to every widget `query` function.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `db` | `DB` | yes | Your database client, exactly as handed to the adapter. |
| `session` | `Session \| null` | yes |  |
| `dateRange` | `object` | yes | Range chosen in the dashboard's date picker, already resolved to dates. |
| `req` | `object` | yes |  |

## Shared option types

`Span` is `1 | 2 | 3 | 4 | 6 | 8 | 12` — the widget's width in the section's
12-column grid.

`NumericFormat` is `"number" | "currency" | "percent" | "bytes" | "duration"`.

`Tone` is `"default" | "accent" | "ok" | "warn" | "err" | "info" | "muted"`.

Every widget accepts `realtime`, a channel name or list. When one fires, that
widget re-runs its query — the rest of the dashboard stays put.

## metric

A single big-number widget.

```ts
export function metric(label: string, query: (ctx: WidgetContext) => Promise<number | string>, options?: MetricOptions): MetricWidget;
```

**MetricOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `icon` | `string` | no | Rendered as-is next to the label. Any string — an emoji, a ligature. No icon-name lookup exists. |
| `format` | `NumericFormat` | no | How the number is rendered. |
| `sublabel` | `string` | no | Small caption under the value. |
| `delta` | `function` | no | Period-over-period change, queried alongside the value. |
| `sparkline` | `function` | no | Trend line drawn inside the card. |
| `tone` | `Tone` | no | Semantic color of the value. |
| `drilldown` | `string` | no | Turns the whole card into a link to this path. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `realtime` | `array \| string` | no | Re-run the query when any of these channels fire. |

`icon` is handed to the metric card as a React child and rendered verbatim.
Metric icons are deliberately free-form: pass an emoji or a ligature your own
icon font resolves; a Lucide name renders as literal text. This differs from
resource, action and command icons, which use the serializable `IconName`
registry described under [resource icons](/docs/reference/resources#icons).

### MetricDelta

**MetricDelta** — Period-over-period change shown under a metric.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `value` | `number` | yes | Fractional change — `0.12` renders as +12%. |
| `vs` | `string` | yes | What the comparison is against, e.g. `"prior period"`. |

## table

A list-of-rows widget on a dashboard.

```ts
export function table<R = unknown>(options: TableWidgetOptions<R>): TableWidget;
```

Point it at a registered `resource` to reuse that resource's columns, or supply
your own `query` and `columns`.

`R` is inferred from `query`, so `columns` is checked against the row that query
returns — a misspelled column is a compile error. `columns` is `RowKey<R>[]`,
which is `keyof R & string` once `R` is known and plain `string` while it is
not: without a `query` there is no row type to check against, and any column
string is accepted.

**TableWidgetOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | no | Card heading. |
| `resource` | `string` | no | Registered resource to pull rows and columns from. |
| `query` | `function` | no | Supply rows yourself instead of reading a resource. |
| `columns` | `array` | no | Columns to show. Defaults to the resource's own list columns. |
| `limit` | `number` | no | Row cap. Applies to the `resource` path only. |
| `emptyState` | `ReactNode` | no | Rendered in place of the table body when there are zero rows. |
| `realtime` | `array \| string` | no | Re-run the query when any of these channels fire. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |

## statGroup

A row of small stats (count + label) — denser than a grid of `metric()` cards.

```ts
export function statGroup(options: StatGroupOptions): StatGroupWidget;
```

**StatGroupOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | no | Card heading. |
| `stats` | `array` | yes | Rows of the group, in order. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `realtime` | `array \| string` | no | Re-run the queries when any of these channels fire. |

### StatItem

**StatItem** — One row of a `statGroup`.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | yes |  |
| `value` | `function \| object \| boolean \| bigint \| number \| string \| null \| undefined` | yes | A literal, or a function resolved per request. |
| `format` | `NumericFormat` | no |  |
| `tone` | `Tone` | no |  |

`StatValue` is `string | number | boolean | bigint | Date | null | undefined`.
Keeping the literal side explicit preserves full `WidgetContext` autocomplete
inside async `value` resolvers.

## custom

Drop a fully-custom React component into a dashboard section.

```ts
export function custom<P>(Component: ComponentType<P>, props: P | ((ctx: WidgetContext) => Promise<P>), options?: CustomOptions): CustomWidget;
```

Renders your own component in the grid. `props` may be a value or a function
resolved on the server per request, which is how you feed it query results.

**CustomOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `realtime` | `array \| string` | no | Re-render when any of these channels fire. |
| `frame` | `boolean` | no | Wrap the component in the standard widget card. |

## Charts

Chart builders live in `@flowpanel/kit/charts`, which is a separate entry point
so the recharts bundle never loads for admins that use no charts.

```ts excerpt
import { areaChart, barChart, lineChart, pieChart } from "@flowpanel/kit/charts";
```

Each takes `(label, query, options)`, where `query` returns an array of rows.
The row type is inferred from `query`, so `x` / `y` (and `category` / `value` on
a pie) must name keys of that row — a typo is a compile error. A `query` typed
as `unknown[]` leaves them as plain strings.

### ChartOptionsBase

Shared by area, bar and line charts.

**ChartOptionsBase** — Charts are defined in
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `x` | `RowKey<R>` | yes | Row key plotted on the x-axis. |
| `y` | `array \| RowKey<R>` | yes | Row key(s) plotted on the y-axis. An array draws one series per key. |
| `height` | `number` | no | Chart height in px. |
| `format` | `NumericFormat` | no | How y-values are rendered in ticks and tooltips. |
| `tooltip` | `"compact" \| "default" \| false` | no | `false` omits the `<Tooltip>` entirely. |
| `drilldown` | `string` | no | Wraps the whole chart card in a link, mirroring MetricCard's whole-card drilldown. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `realtime` | `array \| string` | no | Re-run the query when any of these channels fire. |
| `bucket` | `ChartBucket` | no | X-axis aggregation bucket. |

`ChartBucket` is `"minute" | "hour" | "day" | "week" | "month" | "year" | "auto"`.

### areaChart

```ts
export function areaChart<R = unknown>(label: string, query: (ctx: WidgetContext) => Promise<R[]>, options: AreaChartOptions<R>): AreaChartWidget;
```

**AreaChartOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `stacked` | `boolean` | no | Stack multiple series instead of overlaying them. |
| `smooth` | `boolean` | no | Draw curved rather than straight segments. |
| `x` | `RowKey<R>` | yes | Row key plotted on the x-axis. |
| `y` | `array \| RowKey<R>` | yes | Row key(s) plotted on the y-axis. An array draws one series per key. |
| `height` | `number` | no | Chart height in px. |
| `format` | `NumericFormat` | no | How y-values are rendered in ticks and tooltips. |
| `tooltip` | `"compact" \| "default" \| false` | no | `false` omits the `<Tooltip>` entirely. |
| `drilldown` | `string` | no | Wraps the whole chart card in a link, mirroring MetricCard's whole-card drilldown. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `realtime` | `array \| string` | no | Re-run the query when any of these channels fire. |
| `bucket` | `ChartBucket` | no | X-axis aggregation bucket. |

### barChart

```ts
export function barChart<R = unknown>(label: string, query: (ctx: WidgetContext) => Promise<R[]>, options: BarChartOptions<R>): BarChartWidget;
```

**BarChartOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `stacked` | `boolean` | no | Stack multiple series instead of grouping them side by side. |
| `horizontal` | `boolean` | no | Lay bars out horizontally. |
| `x` | `RowKey<R>` | yes | Row key plotted on the x-axis. |
| `y` | `array \| RowKey<R>` | yes | Row key(s) plotted on the y-axis. An array draws one series per key. |
| `height` | `number` | no | Chart height in px. |
| `format` | `NumericFormat` | no | How y-values are rendered in ticks and tooltips. |
| `tooltip` | `"compact" \| "default" \| false` | no | `false` omits the `<Tooltip>` entirely. |
| `drilldown` | `string` | no | Wraps the whole chart card in a link, mirroring MetricCard's whole-card drilldown. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `realtime` | `array \| string` | no | Re-run the query when any of these channels fire. |
| `bucket` | `ChartBucket` | no | X-axis aggregation bucket. |

### lineChart

```ts
export function lineChart<R = unknown>(label: string, query: (ctx: WidgetContext) => Promise<R[]>, options: LineChartOptions<R>): LineChartWidget;
```

**LineChartOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `smooth` | `boolean` | no | Draw curved rather than straight segments. |
| `markers` | `boolean` | no | Show a dot at every data point. |
| `x` | `RowKey<R>` | yes | Row key plotted on the x-axis. |
| `y` | `array \| RowKey<R>` | yes | Row key(s) plotted on the y-axis. An array draws one series per key. |
| `height` | `number` | no | Chart height in px. |
| `format` | `NumericFormat` | no | How y-values are rendered in ticks and tooltips. |
| `tooltip` | `"compact" \| "default" \| false` | no | `false` omits the `<Tooltip>` entirely. |
| `drilldown` | `string` | no | Wraps the whole chart card in a link, mirroring MetricCard's whole-card drilldown. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `realtime` | `array \| string` | no | Re-run the query when any of these channels fire. |
| `bucket` | `ChartBucket` | no | X-axis aggregation bucket. |

### pieChart

```ts
export function pieChart<R = unknown>(label: string, query: (ctx: WidgetContext) => Promise<R[]>, options: PieChartOptions<R>): PieChartWidget;
```

Pie charts take `category` / `value` instead of `x` / `y`.

**PieChartOptions**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `category` | `RowKey<R>` | yes | Row key naming each slice. |
| `value` | `RowKey<R>` | yes | Row key holding each slice's magnitude. |
| `donut` | `boolean` | no | Cut a hole in the middle. |
| `showLegend` | `boolean` | no | Show the slice legend. |
| `height` | `number` | no | Chart height in px. |
| `span` | `Span` | no | Width in the dashboard's 12-column grid. |
| `drilldown` | `string` | no | Wraps the whole chart card in a link, mirroring MetricCard's whole-card drilldown. |
| `realtime` | `array \| string` | no | Re-run the query when any of these channels fire. |
| `colors` | `Record<string, string>` | no | Pin specific slices to a color, keyed by category value. |
| `format` | `NumericFormat` | no | How slice values are rendered in the tooltip. |
| `tooltip` | `"compact" \| "default" \| false` | no | `false` omits the `<Tooltip>` entirely. |

## Dashboards

Register a dashboard route under `/admin<path>`.

```ts
export function dashboard(config: DashboardConfig): DashboardConfig;
```

Register an arbitrary page under `/admin<path>` that renders a custom React component.

```ts
export function page(config: PageConfig): PageConfig;
```

Both are identity functions — they exist so the object literal is checked
against its type at the call site and reads as a builder alongside `resource()`
and `queue()`.

**DashboardConfig**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | `string` | yes | Path under `basePath`. `"/"` makes this the admin's landing page. |
| `label` | `string` | yes | Nav entry and page title. |
| `icon` | `IconName` | no | Serializable Lucide icon rendered in navigation and the command palette. |
| `dateRange` | `object` | no | Adds a date picker whose value reaches every widget query. |
| `realtime` | `array \| string` | no | SSE channel(s) the whole dashboard subscribes to, independent of any widget. |
| `sections` | `array` | yes | Widget bands, rendered top to bottom. |
| `actions` | `array` | no | Optional top-bar action buttons rendered in the dashboard page header. |
| `hideActionsBar` | `boolean` | no | Hide the default `DashboardActionsBar`. |
| `requireRole` | `function \| array \| string` | no | Restrict access to this dashboard. |

### SectionConfig

**SectionConfig** — A titled band of widgets on a dashboard.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `string` | no | Section heading. Omit for an untitled band. |
| `description` | `string` | no | Supporting copy under the heading. |
| `columns` | `12 \| 6 \| 4 \| 3 \| 2 \| 1` | no | Grid columns the widgets lay out across. |
| `widgets` | `array` | yes |  |

### DateRangeConfig

**DateRangeConfig** — Date picker offered above a dashboard.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `preset` | `DateRangePreset` | no | Range selected on first load. |
| `default` | `object` | no | Explicit range used when no preset is given. |

`DateRangePreset` is `"today" | "yesterday" | "last7d" | "last30d" | "MTD" | "QTD" | "YTD"`.

### ResolvedDateRange

**ResolvedDateRange** — The active range, as every widget query receives it.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | `object` | yes |  |
| `to` | `object` | yes |  |
| `preset` | `"custom" \| "YTD" \| "QTD" \| "MTD" \| "last30d" \| "last7d" \| "yesterday" \| "today"` | yes | `"custom"` once the operator picks their own dates. |

### PageConfig

**PageConfig** — User page registered under `<basePath><path>` with a sidebar nav entry.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | `string` | yes | Path under `basePath`. |
| `label` | `string` | yes | Nav entry and page title. |
| `icon` | `IconName` | no | Serializable Lucide icon rendered in navigation and the command palette. |
| `component` | `ComponentType<Record<string, never>>` | no | Server or client React component rendered at `<basePath><path>`. |
| `href` | `string` | no | External href used when `component` is not provided. |
| `requireRole` | `function \| array \| string` | no | Restrict access to this page. |
