# Dashboard grid

Tiles in as many columns as fit: each column at least `minTileWidth` wide, the columns sharing what is left. A narrow container (a side panel, a phone) gets one column, a wide one as many as fit, with no breakpoints to set; a tile is never narrower than `minTileWidth` unless the grid itself is. Put `DashboardGridItem` around a tile that should span more than one column.

- Registry name: `dashboard-grid` (`@candor/dashboard-grid`)
- 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-grid

## Install

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

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/dashboard-grid`.
It brings in: stack, tokens, utils.
npm packages: @base-ui/react@^1.9.0.

## Example

```tsx
"use client"

import { DashboardGrid, DashboardGridItem } from "@/components/candor/dashboard-grid"
import { StatCard } from "@/components/candor/stat-card"

// Three tiles of different spans. Narrow the window: the wide tile takes the columns there are.
export default function Example() {
  return (
    <DashboardGrid minTileWidth={200} className="w-full">
      <DashboardGridItem span={2}>
        <StatCard kpi={{ label: "Runs this week", value: 1284, delta: { value: 112, vs: "last week", good: "up" } }} />
      </DashboardGridItem>
      <StatCard kpi={{ label: "Failed runs", value: 17 }} />
      <DashboardGridItem span="full">
        <StatCard kpi={{ label: "Cost this month", value: null, missing: "Needs billing access, which isn’t connected." }} />
      </DashboardGridItem>
    </DashboardGrid>
  )
}
```

## <DashboardGrid>

Props:

- `minTileWidth?: number | undefined` (default `280`): The narrowest a tile may be, in pixels. Default 280. The grid fits as many columns of at least this width as it can.
- `gap?: StackGap | undefined` (default `4`): The space between tiles, a step of the spacing scale that follows `data-density`. Default `4`.
- `render?: useRender.RenderProp | undefined`: The element to draw instead of a `<div>`: `render={<section aria-label="Usage" />}`.
- `className?: string | undefined`: Classes for the element, applied last, so they win over the props.
- `style?: React.CSSProperties | undefined`: Styles for the element, applied after the grid's own.
- `children?: React.ReactNode | undefined`: The tiles.

Also takes the props of `React.ComponentProps<"div">`.

## <DashboardGridItem>

A tile that spans more than one column of a `DashboardGrid`. The span is clamped to the columns there are, so a two-column tile in a one-column grid takes the one column rather than overflowing. Until the grid has measured itself (a server render), a spanning tile takes one column; `full` always spans the row.

Props:

- `span?: 1 | 2 | 3 | 4 | "full" | undefined` (default `1`): How many columns the tile takes, at most the grid's columns; `full` takes the whole row. Default 1.
- `render?: useRender.RenderProp | undefined`: The element to draw instead of a `<div>`: `render={<section />}`.
- `className?: string | undefined`: Classes for the element, applied last, so they win over the props.
- `style?: React.CSSProperties | undefined`: Styles for the element; the span's `grid-column` is set before them.
- `children?: React.ReactNode | undefined`: The tile.

Also takes the props of `React.ComponentProps<"div">`.

## Functions

### gridColumns

```ts
gridColumns(width: number, minTile: number, gap: number)
```

How many columns of at least `minTile` pixels, `gap` apart, fit in `width` pixels: never fewer than one. The same sum the grid's `repeat(auto-fit, minmax(min(minTile, 100%), 1fr))` does.

## 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`.
