# Stat Tile

A card with one figure, for the KPI row at the top of a dashboard or list.

```tsx
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

function StatTileDemo() {
  return (
    <div className="grid w-full grid-cols-2 gap-3 md:grid-cols-4">
      <StatTile
        label="Open alerts"
        value="1,204"
        hint="Alerts not yet resolved or dismissed, across all tenants."
        delta={{
          value: "6%",
          trend: "up",
          sentiment: "negative",
          label: "6% more than the previous 24 hours",
        }}
        meta="Last 24 hours"
      />
      <StatTile
        label="Unassigned"
        value="23"
        tone="warning"
        meta="Above the target of 10"
      />
      <StatTile
        label="Past SLA"
        value="7"
        tone="danger"
        meta="Critical and high alerts"
      />
      <StatTile
        label="Time to resolve"
        value="4h 12m"
        hint="Median time from alert creation to resolution."
        delta={{
          value: "38m",
          trend: "down",
          sentiment: "positive",
          label: "38 minutes faster than the previous 30 days",
        }}
        meta="Last 30 days"
      />
    </div>
  )
}
```

## Usage

```tsx
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

export function AlertKpis() {
  return (
    <div className="gap-3 md:grid-cols-4 grid grid-cols-2">
      <StatTile label="Open alerts" value="1,204" meta="Last 24 hours" />
      <StatTile
        label="Past SLA"
        value="7"
        tone="danger"
        meta="Critical and high"
      />
    </div>
  )
}
```

Every KPI row is built from `StatTile`, so figures have the same size and weight
(`text-2xl`, semibold, tabular figures) on every screen. Lay the tiles out in a grid; each tile
fills the height of its row.

- `label` and `value` arrive formatted: the label translated, the value formatted for the
  user's locale ("1,204", "4h 12m"). The tile does not format numbers.
- `unit` sets a unit after the figure, smaller: `value="92" unit="%"`.
- `delta` shows the change against a previous period (see [With a delta](#with-a-delta)).
- `hint` explains how the figure is computed, in a [Help Tooltip](/patterns/help-tooltip).
- `meta` is one line under the figure: the period, the comparison, the source. With a `tone`,
  it says why the figure needs attention.
- `trailing` takes custom content on the figure's line; prefer `unit` and `delta`.

The figure is never colored. Color on the number says "bad" without saying why, turns a row of
tiles into a traffic light, and makes the most prominent text on the screen the hardest to
read. State goes on the reason instead.

The label truncates to one line. Keep it short, and keep a row to four tiles or fewer so labels
fit at common widths.

## Examples

### Needs attention

`tone` reports that the figure needs attention now: a queue above its target, items past their
SLA. The figure keeps its color; the `meta` line, which says why, takes the tone's color and
icon (a triangle for `warning`, an octagon for `danger`), so the state is always read with its
reason. Write `meta` as that reason: "Above the target of 10", not "Warning".

`tone` is a state, not a category. A tile that counts critical vulnerabilities stays `default`
while the count is within its target; marking it because the category is "critical" teaches
users to ignore the marks.

```tsx
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

function StatTileTones() {
  return (
    <div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3">
      <StatTile label="Resolved today" value="58" meta="On track" />
      <StatTile
        label="Awaiting triage"
        value="23"
        tone="warning"
        meta="Above the target of 10"
      />
      <StatTile
        label="Exposed critical assets"
        value="3"
        tone="danger"
        meta="Internet-facing and unpatched"
      />
    </div>
  )
}
```

### With a delta

`delta` shows the change against a previous period, on a soft chip with an arrow:

- `value` is the formatted change ("18%", "38m").
- `trend` (`up`, `down`, `flat`) sets the arrow.
- `sentiment` (`positive`, `negative`, `neutral`, the default) sets the color. Only the app
  knows whether a move is good news: fewer alerts is positive, fewer patched hosts is negative.
  The color follows the sentiment, never the direction.
- `label` is the sentence screen readers announce instead of the bare value: "18% fewer than
  the previous 7 days".

Name the period in `meta`. Hide the delta when there is nothing to compare with, rather than
showing "0%" for missing data. The chip is also exported as `StatDelta`, for a change shown
beside other figures.

```tsx
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

function StatTileWithDelta() {
  return (
    <div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3">
      <StatTile
        label="New alerts"
        value="312"
        delta={{
          value: "18%",
          trend: "down",
          sentiment: "positive",
          label: "18% fewer than the previous 7 days",
        }}
        meta="Compared with the previous 7 days"
      />
      <StatTile
        label="Open vulnerabilities"
        value="1,486"
        delta={{
          value: "9%",
          trend: "up",
          sentiment: "negative",
          label: "9% more than the previous 7 days",
        }}
        meta="Compared with the previous 7 days"
      />
      <StatTile
        label="Monitored assets"
        value="2,031"
        delta={{
          value: "0%",
          trend: "flat",
          label: "No change from the previous 7 days",
        }}
        meta="Compared with the previous 7 days"
      />
    </div>
  )
}
```

### Units and abbreviated figures

`unit` keeps the unit out of the figure, smaller and in secondary text, so a row of
percentages and milliseconds still lines up on the digits. Abbreviate large figures ("1.2M")
only in a tile, and pass the exact figure in `exactValue`: it shows on hover and screen readers
announce it instead of the abbreviation.

```tsx
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

function StatTileUnits() {
  return (
    <div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3">
      <StatTile
        label="Patch coverage"
        value="92"
        unit="%"
        meta="Servers and workstations"
      />
      <StatTile
        label="Ingestion latency"
        value="240"
        unit="ms"
        meta="95th percentile, last hour"
      />
      <StatTile
        label="Events ingested"
        value="1.2M"
        exactValue="1,204,331"
        meta="Last 24 hours"
      />
    </div>
  )
}
```

### No data

`value={null}` shows an [Empty Value](/patterns/empty-value) when there is nothing to count,
such as a mean time with no resolved alert. Never show `0` for missing data: zero is a figure,
and the second tile above is right to show it.

```tsx
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

function StatTileNoData() {
  return (
    <div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
      <StatTile
        label="Mean time to resolve"
        value={null}
        meta="No alert was resolved in the last 30 days"
      />
      <StatTile label="Resolved alerts" value="0" meta="Last 30 days" />
    </div>
  )
}
```

### Loading

`isLoading` replaces the figure, and `meta` when it is set, with skeletons of the same height.
The label stays, so the tile keeps its size and the user knows what is coming.

```tsx
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

function StatTileLoading() {
  return (
    <div className="grid w-full grid-cols-2 gap-3 md:grid-cols-3">
      <StatTile
        label="Monitored assets"
        value=""
        meta="All tenants"
        isLoading
      />
      <StatTile label="Open vulnerabilities" value="" isLoading />
      <StatTile label="Patch coverage" value="" isLoading />
    </div>
  )
}
```

### Clickable

Pass `onClick` to make the whole tile clickable, usually to open the list filtered to what the
figure counts. The tile stretches a real button over the card and keeps the hint's own button
above it, so `onClick` and `hint` work together without nesting one button inside another.

```tsx
import * as React from "react"
import { StatTile } from "@zenoxai/ui/patterns/stat-tile"

function StatTileClickable() {
  const [filter, setFilter] = React.useState<string | null>(null)

  return (
    <div className="flex w-full flex-col gap-3">
      <div className="grid grid-cols-2 gap-3 md:grid-cols-3">
        <StatTile
          label="Unassigned"
          value="23"
          tone="warning"
          meta="No owner yet"
          onClick={() => setFilter("Unassigned alerts")}
        />
        <StatTile
          label="Past SLA"
          value="7"
          tone="danger"
          meta="Critical and high"
          onClick={() => setFilter("Alerts past SLA")}
        />
        <StatTile
          label="Escalated"
          value="12"
          meta="To tier 2"
          onClick={() => setFilter("Escalated alerts")}
        />
      </div>
      <p className="text-sm text-text-secondary">
        {filter ? `Showing: ${filter}` : "Select a tile to filter the list."}
      </p>
    </div>
  )
}
```

## Accessibility

- A clickable tile is a native button covering the card, in the tab order, activated with Enter
  and Space. Its accessible name is the label and the figure with its unit ("Patch coverage:
  92%"), or the exact figure when one is given; the hint stays a separate button.
- Screen readers read the figure as formatted. An abbreviated figure needs `exactValue`.
- A delta's arrow is decorative: give `delta.label` the sentence that says which way it moved.
- `tone` never stands alone: its color and icon mark the `meta` text that explains it.

## API reference

`import { ... } from "@zenoxai/ui/patterns/stat-tile"`

### StatDelta

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| label | `string` |  | Already-translated sentence screen readers announce instead of the bare value, such as "18% fewer than the previous 7 days". |
| sentiment | `"neutral" \| "positive" \| "negative"` | `neutral` | Whether the move is good news. Only the app knows: fewer alerts is positive, fewer patched hosts is negative. Defaults to `neutral`. |
| trend (required) | `"flat" \| "up" \| "down"` |  | Which way the figure moved. Drives the arrow, never the color. |
| value (required) | `string` |  | The change, already formatted for the user's locale ("18%", "+42"). |

### StatTile

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| delta | `StatDeltaProps` |  | Change against a previous period, shown beside the figure. |
| exactValue | `string` |  | The exact figure when `value` is abbreviated ("1.2M"): shown on hover and announced by screen readers instead of the abbreviation. |
| hint | `string` |  | Explanation of how the figure is computed, shown in a tooltip. |
| icon | `ReactNode` |  | Leading icon before the label. |
| isLoading | `boolean` | `false` | Renders skeletons in place of the figure and the meta line. |
| label (required) | `string` |  | Already-translated label above the figure. |
| meta | `ReactNode` |  | Line under the figure: the period, the comparison, the source. With a `tone`, the reason the figure needs attention. |
| onClick | `(() => void)` |  | Makes the whole tile a button, usually to open the filtered list. |
| tone | `"default" \| "warning" \| "danger"` | `default` | Whether the figure needs attention: a queue above its target, items past their SLA. The figure stays neutral; the meta line takes the tone's color and icon, so the state is read together with its reason. |
| trailing | `ReactNode` |  | Custom content on the figure's baseline. Prefer `unit` and `delta`. |
| unit | `string` |  | Unit after the figure, set smaller: "%", "ms", "GB". |
| value (required) | `string \| null` |  | The figure, already formatted for the user's locale ("1,204", "4h 12m"). `null` means there is no data, and the tile shows an empty value instead of a misleading zero. |

