# Dashboard tile

A dashboard tile: a title, when its data was read, the state of that data, and a chart, a stat or a table. Loading, empty, partial and error look as they do in `ChartFrame`; partial shows what exists and says what's missing. A chart inside keeps its title for assistive tech only and draws no frame of its own, so the tile's title isn't said twice. The drill-down is a button whose words say what it filters ("Show the 12 failed runs"); the view switch (drawn with `onViewChange`) offers the views the app passes, and a view the data doesn't allow can stay, locked, saying why.

Unknown stays unknown: without `readAt` there's no stamp at all, and `readAt={null}` says "Never read".

- Registry name: `dashboard-tile` (`@candor/dashboard-tile`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/dashboard-tile

## Install

```bash
npx candor-ui add dashboard-tile
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/dashboard-tile`.
It brings in: button, chart-frame, data, freshness-stamp, heading-level, heading-scope, locale, toggle-group, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { BarChart } from "@/components/candor/bar-chart"
import { LineChart } from "@/components/candor/line-chart"
import { DashboardTile } from "@/components/candor/dashboard-tile"
import { useAllowedViews } from "@/components/candor/chart-views"
import type { Series } from "@/lib/candor/data"

const days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
const runs: Series[] = [{ id: "failed", label: "Failed", values: [2, 4, 1, null, 5] }]

export default function Example() {
  const [view, setView] = React.useState("bar")
  const allowed = useAllowedViews({ categories: days.length, series: runs.length, ordered: true, partsOfWhole: false, negative: false })
  const views = allowed.filter((v) => ["bar", "line", "donut"].includes(v.view)).map((v) => ({ value: v.view, label: v.label, locked: v.reason }))
  const [readAt] = React.useState(() => new Date(Date.now() - 40 * 60 * 1000).toISOString())
  return (
    <DashboardTile
      className="max-w-xl"
      title="Failed runs"
      subtitle="Production · this week"
      readAt={readAt}
      everyHours={1}
      state={{ kind: "partial", note: "Thursday's runs haven't been read yet." }}
      views={views}
      view={view}
      onViewChange={setView}
      drill={{ label: "Show the 12 failed runs", onDrill: () => {} }}
    >
      {view === "line" ? <LineChart title="Failed runs" x={days} series={runs} height={160} /> : <BarChart title="Failed runs" categories={days} series={runs} height={160} />}
    </DashboardTile>
  )
}
```

## <DashboardTile>

Props:

- `title: string`: What the tile shows, as its heading.
- `subtitle?: string | undefined`: A line under the title: the period, the unit, the scope.
- `readAt?: string | null | undefined`: When the data was read (ISO date), shown as a freshness stamp; `null` says "Never read". Left out, no stamp.
- `everyHours?: number | undefined`: How often, in hours, the data should be read again; the stamp says overdue once the data is older than 1.5 times it.
- `state?: ChartState | undefined` (default `{ kind: "ready" }`): Loading, empty (with why), partial (with what's missing) or error (with why). Defaults to ready.
- `onRetry?: (() => void) | undefined`: Shows Try again under an error; called when it's pressed. Pass it only when it loads the data again.
- `drill?: { label: string; onDrill: () => void } | undefined`: A drill-down: a button whose `label` says what it filters ("Show the 12 failed runs"), and what it does.
- `views?: DashboardTileView[] | undefined`: The views to switch between; pass the ones `allowedViews` allows, or lock the others with their reason. The switch is drawn only with more than one view and an `onViewChange`.
- `view?: string | undefined`: The view shown now, one of `views`' values.
- `onViewChange?: ((view: string) => void) | undefined`: Called with the view the person picks. Without it, there's no view switch.
- `toolbar?: React.ReactNode | undefined`: Actions at the tile's top corner: an `ArtifactToolbar`, say.
- `headingLevel?: HeadingLevel | undefined`: The heading's level, to fit the page's outline. Defaults to one below the nearest `HeadingScope`, else 2 (`<h2>`).
- `children?: React.ReactNode | undefined`: The content: a chart, a stat or a table. Shown while ready or partial, in a `HeadingScope` under the tile's title, so a Candor item in it draws its heading one level below.
- `className?: string | undefined`: Classes for the root, merged with Candor's.

## Strings (`dashboardTile`)

The fixed words, English by default. Override any of them under `dashboardTile` in `<LocaleProvider strings={{ dashboardTile: { … } }}>`; the table is exported as `dashboardTileStrings`.

- `views`: View

## Types

### DashboardTileView

One view the tile can switch to: its value and its words; `locked` says why it can't be chosen for this data.

```ts
{
  /** The value `onViewChange` is called with. */
  value: string;
  /** The view's words: "Bars", "Table". */
  label: string;
  /** Why this view can't show this data (from `allowedViews`' `reason`). It stays in the list, saying why. */
  locked?: string | undefined;
}
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### ChartState

Import: `import type { ChartState } from "@/lib/candor/data"` (item `data`, https://candor.themethodic.com/docs/data.md)

```ts
{
  kind: "ready";
} | {
  kind: "loading";
}
/** Nothing to plot, and why. */
 | {
  kind: "empty";
  reason: string;
}
/** Some of the range is missing. */
 | {
  kind: "partial";
  note: string;
} | {
  kind: "error";
  reason: string;
}
```

### HeadingLevel

Import: `import type { HeadingLevel } from "@/lib/candor/heading-level"` (item `heading-level`, https://candor.themethodic.com/docs/heading-level.md)

A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.

```ts
1 | 2 | 3 | 4 | 5 | 6
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
