---
name: dashboard
description: >
  Transverse dashboard reference + scaffolder, consumed DURING development so
  every dashboard uses the right components, the right UI, and correct theming.
  It scaffolds editable, theme-compliant dashboard primitives (KpiCard, ChartCard,
  ListWidget, DashboardGrid, WidgetRenderer) into src/components/dashboard/ —
  because the package's own dashboard components are internal/not exported — and
  it defines the typed-widget config + the per-dashboard data contract that
  scaffold-component renders. Theme tokens come from scaffold-theme (--dataviz-*,
  --chart-*, --kpi-*).
argument-hint: "[scaffold the dashboard primitives, or ask for the dashboard conventions]"
allowed-tools: Read, Grep, Glob, Bash
---

# dashboard — the transverse dashboard contract

Use this skill whenever a dashboard is being built (a BA `SmartDashboard` screen,
or a `dashboard` view on an entity). It guarantees three things during development:
the **right components**, the **right UI**, and **correct theming**.

## Why a dedicated skill (the constraint)

The SmartStack package (`@atlashub/smartstack`) ships dashboard components
(`KpiCard`, `ChartContainer`, ~25 Recharts charts) but they are **internal — not
exported**, so a generated client app cannot import them. Exactly like the
EntityLookup paradox that `scaffold-ui-primitives` solved for form controls, the
answer is to **scaffold local, editable equivalents** the developer owns. This
skill is the dashboard counterpart of `ui-primitives`.

## The primitives (scaffolded into `src/components/dashboard/`)

All token-styled (zero hardcoded colors), editable, `// @customised`-aware:

| File | Role | Key tokens |
|------|------|-----------|
| `types.ts` | `DashboardWidget` + `WidgetResult` — the shared contract (also imported by the page) | — |
| `useDatavizPalette.ts` | reads `--dataviz-1..8` at runtime (Recharts needs real colors in SVG fills) | `--dataviz-*` |
| `KpiCard.tsx` | kpi / counter card (value + optional trend) | `--kpi-value/-label/-trend-up/-trend-down` |
| `ChartCard.tsx` | **the only Recharts wrapper** (`line`/`bar`/`pie`) | `--dataviz-*`, `--chart-grid/-axis/-tooltip-*` |
| `ListWidget.tsx` | top-N table | `--bg-card`, `--border-color`, `--text-*` |
| `DashboardGrid.tsx` | 12-col responsive grid | — |
| `WidgetRenderer.tsx` | switches a typed widget → the right primitive; unknown/no-data → muted placeholder | — |

Recharts must be a dependency of the consuming app (`npm i recharts`) — the CLI
warns if it's missing.

## The widget config (`DashboardWidget`)

```ts
type DashboardWidgetType = 'kpi' | 'counter' | 'chart-line' | 'chart-bar' | 'chart-area' | 'chart-pie' | 'list';
interface DashboardWidget {
  key: string;            // stable key + the data key
  label: string;          // already translated by the page
  type: DashboardWidgetType;
  col?: number;           // 1-12 span (defaults: kpi 3, charts 6, list 12)
  endpoint?: string;      // data source, e.g. 'widgets/active'
  permission?: string;    // gate visibility
  entity?: string; aggregation?: 'count'|'sum'|'avg'|'min'|'max'; field?: string; // BA hints
}
```
This mirrors the BA `SmartDashboard` widget vocabulary (see
`business-analyse/create-screen/references/smartcomponents.md` §SmartDashboard).

## The data contract (one endpoint per dashboard)

The page fetches **once** and hands `data[widget.key]` to each `<WidgetRenderer>`:

```
GET /api/screens/{dashboard-slug}/dashboard?from&to  →  { widgets: Record<widgetKey, WidgetResult> }

WidgetResult = {
  value?, delta?, unit?,                      // kpi / counter
  points?: { label, [series]: number }[],     // chart-line / chart-bar / chart-area / kpi sparkline
  slices?: { name, value }[],                  // chart-pie
  rows?, columns?,                             // list
}
```

The backend has **no generic aggregation engine** — each dashboard is a dedicated
MediatR Query returning this DTO (one widget key per result). Until a key is
returned, that widget renders the muted "no data" placeholder, so the dashboard is
shippable before every endpoint exists. **Never** reintroduce the old
`/dashboard/consolidated` + flat `consolidated.metrics` contract (it had no
backend) — `audit-dev-frontend` DEV-UI-036 (`stale-dashboard-contract`) flags it.

## Invocation (scaffold the primitives — idempotent)

```bash
npx --prefer-offline tsx skills/development/frontend/dashboard/cli/scaffold-dashboard-primitives/index.ts \
  --spec '{ "projectPath": "/abs/project", "appCode": "demo" }'
```
Add `--dry_run` to preview. `force:true` overwrites non-`@customised` files. Run it
once per app, before scaffolding any dashboard page (so `@/components/dashboard/*`
resolves). Deep-merges the `dashboard.*` i18n namespace (siblings preserved).

## Theming — the colors ALWAYS come from the theme

Colors come exclusively from theme tokens emitted by **`/scaffold-theme`**:
`--dataviz-1..8` (categorical, hue-rotated from the accent — never status tokens,
R17), `--chart-grid/-axis/-tooltip-bg/-text`, `--kpi-value/-label/-trend-up/-trend-down`.
If a project needs custom category colors, pass `theme.dataviz: string[]` to
scaffold-theme — do NOT hardcode hex in a widget.

**Why a hook and not `var()`**: Recharts writes real color strings into SVG fills,
so `useDatavizPalette()` reads the tokens off `:root` at runtime and re-reads them
on any theme change (it observes BOTH the `class` and the `style` attribute of
`<html>` — the platform theme runtime writes its tokens as inline styles, so
watching the dark-mode class alone would miss a live theme switch). Resolution
order, theme-first:

1. **`--dataviz-cat-1..12`** — the LIVE categorical palette the
   `@atlashub/smartstack` theme runtime (`ThemeContext.applyDataViz`) writes on
   `<html>` from the tenant's **UI configuration** (`UiTheme.dataVizColorsJson`,
   optionally overridden per `UiPreset`). This is the authoritative source
   whenever the app runs the platform theme: change the theme in the admin UI and
   the charts follow immediately. The socle's own charts use the same channel
   (`getCategoricalColor` → `readVar('--dataviz-cat-N')`);
2. `--dataviz-1..8` — the palette scaffold-theme bakes into `index.css`. Each slot
   is itself emitted as `var(--dataviz-cat-N, <accent-derived hex>)`, so the two
   layers agree by construction;
3. the accent ramp `--color-accent-*` (also written live by the theme runtime) —
   used when the project's `index.css` predates the dataviz block or carries
   `/* @customised */` (which makes scaffold-theme skip the file). Monochrome, but
   still the app's own colors;
4. a built-in last-resort palette — only when no SmartStack theme is readable.

Levels 3-4 `console.warn` once and are flagged by the audits below: a chart must
never silently render foreign hues.

The chrome follows the same rule — scaffold-theme emits
`--chart-tooltip-bg: var(--dataviz-surface-elevated, var(--bg-card, …))`,
`--chart-grid: var(--border-subtle, …)`, `--chart-axis: var(--text-muted, …)`,
`--kpi-trend-up: var(--dataviz-trend-up, var(--success-text))` — so tooltips,
axes and trend arrows inherit the UI-configuration theme, not a frozen grey.

**Enforcement (both nets run the same contract):**

| Gate | Rule | Fires on |
|------|------|----------|
| `audit-dev-frontend` (ba-develop Phase 3 gate) | `DEV-UI-036` | theme defining neither `--dataviz-1` nor `--dataviz-cat-1`, color literal in a dashboard page/primitive, `recharts` imported outside `ChartCard.tsx`, stale `/dashboard/consolidated` contract |
| `/ui-components` → ui-polish | `R28-dataviz-hardcoded-colors` | same three color shapes, page by page (error on literals, warning on raw Recharts / a theme with no palette) |

The missing-palette shape is a **warning, not an error, when `@atlashub/smartstack`
is installed**: its theme runtime injects `--dataviz-cat-*` as inline styles, which
no static CSS scan can see — the honest verdict is "verify in the browser".

Both exempt `ChartCard.tsx` (the sanctioned wrapper) and `useDatavizPalette.*`
(its documented fallback constant).

## How it's consumed during development

- **ba-develop Phase 3.0** runs `scaffold-dashboard-primitives` right after
  `scaffold-ui-primitives` (so the page's imports resolve).
- **scaffold-component** (`dashboard` view) renders `pageSpec.widgets[]` through
  `<DashboardGrid>` + `<WidgetRenderer>` — it does not hand-roll KPI markup.
- **audit**: `audit-dev-frontend` **DEV-UI-036** (ba-develop gate) + ui-polish
  **R28** (`/ui-components`) enforce the primitives + dataviz tokens — no raw
  Recharts, no hardcoded chart colors, no theme without `--dataviz-*`.

## When NOT to use

- Form controls (datepicker, dropdown, FK lookup) → `/scaffold-ui-primitives`.
- App/module/section landing pages with quickLinks → the `SmartHome*` screen types
  (navigational), not `SmartDashboard` (free-form analytics).
- Restyle the theme → `/scaffold-theme`.

## 3.4 uplift — area / stacked / sparkline / drill-down

- `chart-area` renders an AreaChart in ChartCard; `stacked: true` stacks the
  series on bar/area charts (stackId).
- `sparkline: true` on a kpi/counter renders a 40px axis-less trend under the
  figure when the widget result carries `points` — through `Sparkline.tsx`,
  the ONE other sanctioned recharts location besides ChartCard (DEV-UI-036 and
  ui-polish R28 whitelist both).
- `WidgetRenderer` accepts `onOpen` — the page wires it from the pagespec's
  `widgets[].drillTo` (+ `drillParams`, preset URL-state on the target list):
  the whole widget becomes a keyboard-able affordance (testid
  `widget-open-<key>`) opening the backing list view.
