
# Lists

> The list component — a vertical run of records drawn from a per-record template, with Load More paging.

A `list` draws one entry per record, from a template you declare. Everything about presentation lives under `listDisplay`; what is fetched, and how much of it, belongs to `dataSource`.

### `dataSource`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `dataSource` | object |  |  | DB-table binding (DataSource) OR a system read-endpoint binding |
| `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: feed a data-bound component from a system endpoint instead of a DB table |
| `dataSource.system.endpoint` | string |  |  | Read endpoint path to fetch rows from (e.g. /api/admin/automations/runs) |
| `dataSource.system.rowsKey` | string |  |  | Key of the rows array in the response envelope (default: 'items') |
| `dataSource.system.param` | string |  |  | Route parameter substituted into the endpoint's :param placeholder. Must be declared by the host page's path. |
| `dataSource.system.idKey` | string |  |  | Key of each row's unique id (default: 'id') |
| `dataSource.system.totalKey` | string |  |  | Key of the total-count in the envelope; falls back to rows length if absent |
| `dataSource.system.query` | object |  |  | Static query params merged into every request to the endpoint |
| `dataSource.system.bindTo` | string |  |  | ID of a sibling shared filter/period selector whose published params are merged into every request to endpoint (the dynamic counterpart to the static query) |
| `dataSource.system.sharedFilter` | object (truncated) |  |  | Companion to bindTo: the shared selector params merged (dynamically) into every request to endpoint, alongside the static query. Inert without bindTo. |

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

### `listDisplay`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `listDisplay` | object |  |  | Search-first result display configuration for list components with item templates and pagination |
| `listDisplay.itemTemplate` | object |  |  | Template for rendering each record as a list item with $record.* variables |
| `listDisplay.itemTemplate.title` | string |  |  | Primary text using $record.* variable reference |
| `listDisplay.itemTemplate.subtitle` | string |  |  | Secondary text using $record.* variable reference |
| `listDisplay.itemTemplate.image` | string |  |  | Image URL using $record.* variable reference |
| `listDisplay.itemTemplate.badge` | string |  |  | Badge text using $record.* variable reference |
| `listDisplay.itemTemplate.metadata` | array (truncated) |  |  | Metadata fields displayed in the list item footer |
| `listDisplay.emptyMessage` | string |  |  | Message when no search results match |
| `listDisplay.loadMore` | enum | `button`, `infinite` |  | Pagination: 'button' shows a Load More button, 'infinite' loads on scroll |
| `listDisplay.highlight` | boolean |  |  | Highlight matched search terms in list item text (default: false) |
| `listDisplay.divider` | boolean |  |  | Show visual dividers between list items (default: false) |
| `listDisplay.maxItems` | number |  |  | Maximum number of items to display |

```yaml
tables:
  - name: articles
    fields:
      - { name: title, type: single-line-text }
      - { name: published_at, type: datetime }
pages:
  - name: Articles
    path: /articles
    components:
      - type: list
        dataSource:
          table: articles
          sort: [{ field: published_at, direction: desc }]
          limit: 20
        listDisplay:
          itemTemplate:
            title: '$record.title'
            metadata: [{ field: published_at, format: relative-date }]
          loadMore: button
```

`itemTemplate` takes `title`, `subtitle`, `image`, `badge` and `metadata`, where `metadata` is an array of `{ field, format }` rendered in the item footer.

## Paging is the binding's business, not the display's

Page size comes from `dataSource.limit`: it sets how many records the first page holds, and each press of Load More appends another page of that size.

`loadMore: button` renders the control that does the appending. On a `dataSource.system` binding it appears only if that binding also declares `totalKey` **and** the endpoint answers a number there — without one the reported total is the page's own length, so there is never anything left to load.

`maxItems` caps how many records the list DRAWS. It does not change what is fetched — a page is transport, a cap is display — so a list at its cap hides the Load More control, since anything the next page brought would be cut off on arrival. To fetch fewer records, set `dataSource.limit` instead.

## Three keys are accepted and then ignored

Stated here rather than left to be discovered: `loadMore: infinite`, `highlight` and `divider` have **no effect**. Each decodes cleanly and changes nothing, so a list declaring `loadMore: infinite` pages exactly like one declaring nothing at all.

They are documented because an unread key is the hardest kind of config to debug: it validates, it renders, and the behaviour it names never arrives. If you need infinite scrolling today, `loadMore: button` is what exists.
