
# KPI Cards

> The kpi component — a single summary metric, with an optional comparison, sparkline and thresholds.

A KPI card states one number. It aggregates the records its `dataSource` binds and formats the result.

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `label` | string |  |  | Descriptive text displayed above the KPI metric value |
| `icon` | string |  |  | Lucide icon name displayed alongside the KPI metric (e.g., dollar-sign) |

### `dataSource`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `dataSource` | object |  |  | Data binding for the KPI: a DB table (aggregated client-side) OR a system read endpoint (pre-computed scalar value-path) |
| `dataSource.table` | string |  |  | Table to bind to: a declared name (validated against app.tables), or a $param.<name> route reference declared by the page path |
| `dataSource.fields` | array |  |  | Specific fields to fetch from the table |
| `dataSource.fields[]` | string |  |  | One field name, spelled as the bound table declares it |
| `dataSource.mode` | enum | `list`, `single`, `search` |  | Data fetching mode: 'list' (multiple), 'single' (one record), 'search' (interactive) |
| `dataSource.filter` | array |  |  | Filter conditions applied with AND logic |
| `dataSource.filter[]` | object (truncated) |  |  | Single filter condition for data source queries |
| `dataSource.sort` | array |  |  | Sort rules applied in order |
| `dataSource.sort[]` | object (truncated) |  |  | Single sort rule for data source queries |
| `dataSource.pagination` | object |  |  | Pagination configuration for data source |
| `dataSource.pagination.pageSize` | number |  |  | Number of records per page |
| `dataSource.pagination.style` | enum | `numbered`, `loadMore`, `infinite` |  | How pagination controls are displayed (default: numbered). `infinite` is accepted but not implemented and pages as `numbered`. |
| `dataSource.param` | string |  |  | Route parameter name for single mode (e.g., slug, id) |
| `dataSource.searchEngine` | enum | `client`, `fts`, `trigram`, `hybrid` |  | Search backend for this data source (default: 'client'). Only 'client' is dispatched today; the other three validate and search as 'client' does. |
| `dataSource.searchFields` | array |  |  | Fields to search across in search mode |
| `dataSource.searchFields[]` | string |  |  | One field name the search term is matched against |
| `dataSource.debounceMs` | number |  |  | Debounce delay for search input (ms) |
| `dataSource.limit` | number |  |  | Maximum number of results to return |
| `dataSource.targetId` | string |  |  | Publisher-side identifier for cross-component references — addressable by a FilterAction (targetDataSource) and by a sibling subscriber's bindTo (shared filter/period state) |
| `dataSource.bindTo` | string |  |  | ID of a publisher component whose value drives this data source (cross-component binding). By default a `search-input` whose query string drives the search; when sharedFilter is also set, a shared filter/period selector whose published params are merged into every request |
| `dataSource.sharedFilter` | object |  |  | Companion to bindTo: the bound publisher is a shared filter/period selector whose published params are merged into every request this data source issues. One selector can drive many sibling subscribers. Inert without bindTo. |
| `dataSource.sharedFilter.params` | array (truncated) |  |  | Request-param keys this subscriber consumes from the shared publisher's value bag (omit to merge the full bag verbatim) |
| `dataSource.refreshMode` | enum | `none`, `poll`, `realtime` |  | Data refresh strategy for this binding (default: 'none'). 'poll' uses pollIntervalMs; 'realtime' subscribes to live change events. |
| `dataSource.pollIntervalMs` | number |  | 30000 | Polling interval in milliseconds for refreshMode: poll (min 1000, max 300000). Defaults to 30000 when omitted, and is ignored unless refreshMode is 'poll'. |
| `dataSource.system` | object |  |  | Read-endpoint binding: read a single pre-computed scalar (or a value template) from a system endpoint instead of aggregating a DB table |
| `dataSource.system.endpoint` | string |  |  | Read endpoint path to fetch the scalar from (e.g. /api/admin/overview) |
| `dataSource.system.valuePath` | string |  |  | Dotted path to a single scalar in the fetched envelope |
| `dataSource.system.valueTemplate` | string |  |  | Template interpolating {dotted.path} tokens from the fetched envelope |
| `dataSource.system.query` | object |  |  | Static query params merged into the request to the endpoint |

### `autoSave`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `autoSave` | object |  |  | Configuration for automatic persistence of edits. Applies to table, form, kanban, and calendar components. |
| `autoSave.saveMode` | enum | `auto`, `onBlur`, `manual` |  | Save trigger strategy: 'auto' (debounced), 'onBlur' (field blur), 'manual' (button). Default: 'manual'. |
| `autoSave.autoSaveDebounceMs` | number |  |  | Debounce delay for auto-save in milliseconds (default: 500, min: 100) |
| `autoSave.showSaveIndicator` | boolean |  |  | Display a save status indicator (Saving... / Saved / Error). Default: true when saveMode is auto or onBlur. |
| `autoSave.saveIndicatorPosition` | enum | `inline`, `toast`, `toolbar` |  | Where the save status indicator appears |

### `search`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `search` | object |  |  | Shared search bar configuration for data-bound components (table, kanban, calendar) |
| `search.enabled` | boolean |  |  | Enable search bar (default: true) |
| `search.placeholder` | string |  |  | Search input placeholder text |
| `search.debounceMs` | number |  |  | Debounce delay for search input in ms (default: 300) |
| `search.highlight` | boolean |  |  | Highlight matched search terms in results (default: false) |

### `kpiAggregate`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `kpiAggregate` | object |  |  | Aggregate function configuration for KPI metric computation |
| `kpiAggregate.function` | enum | `count`, `sum`, `avg`, `min`, `max` |  | Aggregate function applied to compute the KPI metric value |
| `kpiAggregate.field` | string |  |  | Field to aggregate (omit for count) |

### `trend`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `trend` | object |  |  | Trend comparison showing change direction and percentage from a previous period |
| `trend.comparisonPeriod` | enum | `previousDay`, `previousWeek`, `previousMonth`, `previousQuarter`, `previousYear` |  | Time period to compare the current metric value against |
| `trend.direction` | enum | `up`, `down`, `flat` |  | Direction of change compared to the previous period |
| `trend.changePercent` | number |  |  | Percentage change from the comparison period |
| `trend.color` | enum | `green`, `red`, `yellow`, `gray` |  | Color indicating whether the trend is positive, negative, or neutral |

### `kpiFormat`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `kpiFormat` | object |  |  | Display formatting configuration for the KPI metric value |
| `kpiFormat.type` | enum | `number`, `currency`, `percentage`, `compact`, `bytes` |  | Display format for the KPI metric value |
| `kpiFormat.options` | object |  |  | Additional format options (e.g., { currency: "USD" }) |

### `thresholds`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `thresholds` | array |  |  | Conditional color thresholds for KPI value |
| `thresholds[]` | object |  |  | Conditional color threshold — applied when the metric value meets or exceeds the boundary |
| `thresholds[].value` | number |  |  | Threshold boundary value |
| `thresholds[].color` | enum | `red`, `green`, `yellow`, `blue`, `gray` |  | Colour applied when the metric value meets this boundary |

### `sparkline`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `sparkline` | object |  |  | Mini line chart showing recent trend for the KPI metric |
| `sparkline.field` | string |  |  | Field to plot in the sparkline |
| `sparkline.groupBy` | string |  |  | Date field for grouping data points |
| `sparkline.interval` | enum | `day`, `week`, `month` |  | Date grouping interval for sparkline data points |
| `sparkline.days` | number |  |  | Number of trailing days of data to show |

`label` is the metric's name and `icon` draws beside the value. `kpiAggregate` is `{ function, field }` — `function` is required, and `field` is omitted for `count`. `kpiFormat` is `{ type, options }`, where `type` is `number`, `currency`, `percentage`, `compact` or `bytes`. `thresholds` are `{ value, color }` entries that recolour the card once a value is crossed, over `red`, `green`, `yellow`, `blue` and `gray`.

```yaml
tables:
  - name: orders
    fields:
      - { name: total, type: currency, currency: EUR }
      - { name: created_at, type: created-at }
pages:
  - name: Revenue
    path: /revenue
    components:
      - type: kpi
        label: Revenue this month
        icon: banknote
        dataSource: { table: orders }
        kpiAggregate: { function: sum, field: total }
        kpiFormat: { type: currency, options: { currency: EUR } }
        trend: { comparisonPeriod: previousMonth, direction: up, changePercent: 12.4, color: green }
        sparkline: { field: total, groupBy: created_at, interval: day, days: 30 }
```

## `trend`

All three of `comparisonPeriod`, `direction` and `changePercent` are required once `trend` is present. `comparisonPeriod` is `previousDay`, `previousWeek`, `previousMonth`, `previousQuarter` or `previousYear`; `direction` is `up`, `down` or `flat`; `changePercent` is the change as a number; and `color` is `green`, `red`, `yellow` or `gray`.

**`direction` and `color` are independent on purpose.** Revenue up is green; churn up is red. Sovrium will not guess which of your metrics improve by rising, so state the colour you mean.

## `sparkline`

All four properties are required once `sparkline` is present: `field` is plotted along the line, `groupBy` buckets the points, `interval` is `day`, `week` or `month`, and `days` is how far back the line covers.

## Behaviour

### KPI Card

- `type: 'kpi'` is a valid component type in page components
- `dataSource` with `aggregate` (count/sum/avg/min/max) computes metric value
- `label` displays descriptive text above the metric value
- `format.type` (number/currency/percentage/compact) controls display format
- `icon` renders a Lucide icon name alongside the metric
- `trend` shows comparison arrow (up/down), percentage change, and color
- A metric takes a threshold's colour only once it meets that boundary
- A threshold colour outside the drawable vocabulary is refused
- User can complete full KPI card workflow (regression)

### KPI Grid Layout

- Multiple KPI cards render in a responsive grid layout
- Grid column count adapts per breakpoint (1 mobile, 2-4 desktop)
- `sparkline` renders a mini line chart showing recent trend for the metric
- `thresholds` apply conditional color to the metric value based on thresholds
- A `sparkline` on a system binding is refused outright rather than drawn empty
- User can complete full KPI grid workflow (regression)

### KPI on Public Pages (Anonymous Visitors)

- KPI `label` is server-rendered as visible text in the SSR HTML (not only inside `data-island-props`)
- Anonymous visitor on a public page sees the label and a graceful value; no `KpiError` region replaces it
- User can complete the full public/anonymous KPI workflow (regression)
