
# Calendars

> The calendar component — a month, week or day view of date-bearing records.

A calendar places each record on the date its `dateField` names. The field mappings sit at the top level; only click handling and the time-grid settings live under `calendarEvent` and `calendarInteraction`.

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `dateField` | string |  |  | Date/datetime field for calendar event position |
| `endDateField` | string |  |  | End date field for multi-day calendar events |
| `defaultView` | enum | `month`, `week`, `day` |  | Calendar display mode: month grid, week view, or day view |
| `labelField` | string |  |  | Field to use as event label on calendar |
| `colorField` | string |  |  | Field whose values colour each event. A value whose option declares no colour is given one from a fixed fallback palette, hashed from the value — unlike a table’s rowColorField, which fills only from declared option colours. The hash is stable per value and distinct values may collide. |
| `maxEventsPerDay` | number |  |  | Max events visible per day cell before "+N more" |

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

### `calendarEvent`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `calendarEvent` | object |  |  | Configuration for how events are displayed and interacted with on the calendar |
| `calendarEvent.onEventClick` | object |  |  | Component action. Discriminated by type: auth (authentication), crud (data operations), automation (invoke workflow), filter (cross-component filtering), navigate (pure URL navigation), toast (transient notification), fetch (client-side HTTP with toast response). Open-drawer is discriminated by `action: openDrawer` instead of `type` (PG-04 quick-edit drawer pattern). |
| `calendarEvent.onEventClick.type` | enum | `auth`, `crud`, `automation`, `filter`, `navigate`, `toast`, `fetch` |  | Which kind of action this is. It decides which of the other keys apply. |
| `calendarEvent.onEventClick.method` | enum | `login`, `signup`, `logout`, `resetPassword`, `setNewPassword`, `verifyEmail`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |  | What the action performs: the authentication operation under `type: auth`, or the HTTP verb under `type: fetch` (default GET). |
| `calendarEvent.onEventClick.strategy` | enum | `email`, `magicLink`, `oauth` |  | Authentication strategy to use |
| `calendarEvent.onEventClick.provider` | string |  |  | OAuth provider name (e.g., google, github) |
| `calendarEvent.onEventClick.submitLabel` | string |  |  | Submit-button label for the form this action embeds. Defaults to the localized built-in label for the method or operation. Supports $t:key translation references. |
| `calendarEvent.onEventClick.pendingLabel` | string |  |  | Custom in-flight (pending) submit-button label for the auth form. Defaults to the localized built-in pending label for the method. Supports $t:key translation references. |
| `calendarEvent.onEventClick.fields` | array (truncated) |  |  | Per-field label/placeholder overrides for the form this action embeds. Targets fields by name. Additive — the default fields are used when omitted. |
| `calendarEvent.onEventClick.onSuccess` | object (truncated) |  |  |  |
| `calendarEvent.onEventClick.onError` | object (truncated) |  |  |  |
| `calendarEvent.onEventClick.operation` | enum | `create`, `update`, `delete` |  | Data operation to perform |
| `calendarEvent.onEventClick.table` | string |  |  | Table to perform the operation on |
| `calendarEvent.onEventClick.confirm` | boolean |  |  | If true, shows a confirmation prompt before executing the action |
| `calendarEvent.onEventClick.confirmMessage` | string |  |  | Custom confirmation message. Defaults to a generic confirmation prompt. |
| `calendarEvent.onEventClick.data` | object |  |  | Field values to apply in bulk update operations |
| `calendarEvent.onEventClick.name` | string |  |  | Automation name (must match an automation defined in app.automations) |
| `calendarEvent.onEventClick.inputData` | object |  |  | Key-value pairs passed to the automation as input. Supports $variable references. |
| `calendarEvent.onEventClick.await` | boolean |  |  | Wait for completion before triggering response (default: false = fire-and-forget) |
| `calendarEvent.onEventClick.targetDataSource` | string |  |  | ID of the data source to filter (matches dataSource.targetId) |
| `calendarEvent.onEventClick.field` | string |  |  | Field name to apply the filter to |
| `calendarEvent.onEventClick.operator` | enum | `eq`, `neq`, `contains`, `gt`, `lt`, `gte`, `lte` |  | Comparison operator (defaults to eq) |
| `calendarEvent.onEventClick.path` | string |  |  | Destination URL path (supports $record.X substitution) |
| `calendarEvent.onEventClick.message` | string |  |  | Toast notification message. Supports $variable references. |
| `calendarEvent.onEventClick.variant` | enum | `success`, `error`, `warning`, `info` |  | Visual style of the toast notification |
| `calendarEvent.onEventClick.duration` | number |  |  | Auto-dismiss duration in milliseconds (default: 5000) |
| `calendarEvent.onEventClick.url` | string |  |  | Target URL (any absolute path or fully-qualified URL; not prefix-restricted). e.g. /api/tables/contacts/records, /api/auth/admin/ban-user, /api/buckets/default/files/<key> |
| `calendarEvent.onEventClick.mode` | enum | `fetch`, `navigate`, `download`, `oauth` |  | Dispatch mode: fetch (default, client fetch + toast), navigate (browser navigation, e.g. ?format=csv export), download (native file download), oauth (authorize → provider → callback round-trip) |
| `calendarEvent.onEventClick.headers` | object |  |  | Request headers. Content-Type defaults to application/json when body is set. |
| `calendarEvent.onEventClick.body` | object |  |  | JSON request body (serialized with JSON.stringify). String values support $record.<field> and the $session.<field> token (resolved client-side from the caller session, e.g. { confirm: "$session.email" }). |
| `calendarEvent.onEventClick.filename` | string |  |  | Suggested download filename. Only meaningful when mode is "download". |
| `calendarEvent.onEventClick.redirectKey` | string |  |  | Response field holding the OAuth provider redirect URL (default "url"). Only meaningful when mode is "oauth". |
| `calendarEvent.onEventClick.callbackPath` | string |  |  | OAuth provider return path (the registered redirect_uri). Only meaningful when mode is "oauth". |
| `calendarEvent.onEventClick.responseEnvelope` | enum | `sovrium`, `better-auth`, `raw` | sovrium | Response-envelope interpretation: sovrium (default), better-auth (always-200 enumeration-safe envelope at /api/auth/admin/*), raw (status-only, no body assumptions) |
| `calendarEvent.onEventClick.action` | enum | `openDrawer` |  | Set to `openDrawer` to open a sibling drawer instead of running one of the `type` actions. |
| `calendarEvent.onEventClick.component` | string |  |  | ID of the drawer page-component to open (matches a sibling `{ type: 'drawer', id }`) |
| `calendarEvent.onEventClick.props` | object (truncated) |  |  | Per-trigger overrides applied to the referenced drawer component |

### `calendarInteraction`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `calendarInteraction` | object |  |  | Configuration for calendar user interactions (date clicks, drag, time slots) |
| `calendarInteraction.onDateClick` | object |  |  | Component action. Discriminated by type: auth (authentication), crud (data operations), automation (invoke workflow), filter (cross-component filtering), navigate (pure URL navigation), toast (transient notification), fetch (client-side HTTP with toast response). Open-drawer is discriminated by `action: openDrawer` instead of `type` (PG-04 quick-edit drawer pattern). |
| `calendarInteraction.onDateClick.type` | enum | `auth`, `crud`, `automation`, `filter`, `navigate`, `toast`, `fetch` |  | Which kind of action this is. It decides which of the other keys apply. |
| `calendarInteraction.onDateClick.method` | enum | `login`, `signup`, `logout`, `resetPassword`, `setNewPassword`, `verifyEmail`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |  | What the action performs: the authentication operation under `type: auth`, or the HTTP verb under `type: fetch` (default GET). |
| `calendarInteraction.onDateClick.strategy` | enum | `email`, `magicLink`, `oauth` |  | Authentication strategy to use |
| `calendarInteraction.onDateClick.provider` | string |  |  | OAuth provider name (e.g., google, github) |
| `calendarInteraction.onDateClick.submitLabel` | string |  |  | Submit-button label for the form this action embeds. Defaults to the localized built-in label for the method or operation. Supports $t:key translation references. |
| `calendarInteraction.onDateClick.pendingLabel` | string |  |  | Custom in-flight (pending) submit-button label for the auth form. Defaults to the localized built-in pending label for the method. Supports $t:key translation references. |
| `calendarInteraction.onDateClick.fields` | array (truncated) |  |  | Per-field label/placeholder overrides for the form this action embeds. Targets fields by name. Additive — the default fields are used when omitted. |
| `calendarInteraction.onDateClick.onSuccess` | object (truncated) |  |  |  |
| `calendarInteraction.onDateClick.onError` | object (truncated) |  |  |  |
| `calendarInteraction.onDateClick.operation` | enum | `create`, `update`, `delete` |  | Data operation to perform |
| `calendarInteraction.onDateClick.table` | string |  |  | Table to perform the operation on |
| `calendarInteraction.onDateClick.confirm` | boolean |  |  | If true, shows a confirmation prompt before executing the action |
| `calendarInteraction.onDateClick.confirmMessage` | string |  |  | Custom confirmation message. Defaults to a generic confirmation prompt. |
| `calendarInteraction.onDateClick.data` | object |  |  | Field values to apply in bulk update operations |
| `calendarInteraction.onDateClick.name` | string |  |  | Automation name (must match an automation defined in app.automations) |
| `calendarInteraction.onDateClick.inputData` | object |  |  | Key-value pairs passed to the automation as input. Supports $variable references. |
| `calendarInteraction.onDateClick.await` | boolean |  |  | Wait for completion before triggering response (default: false = fire-and-forget) |
| `calendarInteraction.onDateClick.targetDataSource` | string |  |  | ID of the data source to filter (matches dataSource.targetId) |
| `calendarInteraction.onDateClick.field` | string |  |  | Field name to apply the filter to |
| `calendarInteraction.onDateClick.operator` | enum | `eq`, `neq`, `contains`, `gt`, `lt`, `gte`, `lte` |  | Comparison operator (defaults to eq) |
| `calendarInteraction.onDateClick.path` | string |  |  | Destination URL path (supports $record.X substitution) |
| `calendarInteraction.onDateClick.message` | string |  |  | Toast notification message. Supports $variable references. |
| `calendarInteraction.onDateClick.variant` | enum | `success`, `error`, `warning`, `info` |  | Visual style of the toast notification |
| `calendarInteraction.onDateClick.duration` | number |  |  | Auto-dismiss duration in milliseconds (default: 5000) |
| `calendarInteraction.onDateClick.url` | string |  |  | Target URL (any absolute path or fully-qualified URL; not prefix-restricted). e.g. /api/tables/contacts/records, /api/auth/admin/ban-user, /api/buckets/default/files/<key> |
| `calendarInteraction.onDateClick.mode` | enum | `fetch`, `navigate`, `download`, `oauth` |  | Dispatch mode: fetch (default, client fetch + toast), navigate (browser navigation, e.g. ?format=csv export), download (native file download), oauth (authorize → provider → callback round-trip) |
| `calendarInteraction.onDateClick.headers` | object |  |  | Request headers. Content-Type defaults to application/json when body is set. |
| `calendarInteraction.onDateClick.body` | object |  |  | JSON request body (serialized with JSON.stringify). String values support $record.<field> and the $session.<field> token (resolved client-side from the caller session, e.g. { confirm: "$session.email" }). |
| `calendarInteraction.onDateClick.filename` | string |  |  | Suggested download filename. Only meaningful when mode is "download". |
| `calendarInteraction.onDateClick.redirectKey` | string |  |  | Response field holding the OAuth provider redirect URL (default "url"). Only meaningful when mode is "oauth". |
| `calendarInteraction.onDateClick.callbackPath` | string |  |  | OAuth provider return path (the registered redirect_uri). Only meaningful when mode is "oauth". |
| `calendarInteraction.onDateClick.responseEnvelope` | enum | `sovrium`, `better-auth`, `raw` | sovrium | Response-envelope interpretation: sovrium (default), better-auth (always-200 enumeration-safe envelope at /api/auth/admin/*), raw (status-only, no body assumptions) |
| `calendarInteraction.onDateClick.action` | enum | `openDrawer` |  | Set to `openDrawer` to open a sibling drawer instead of running one of the `type` actions. |
| `calendarInteraction.onDateClick.component` | string |  |  | ID of the drawer page-component to open (matches a sibling `{ type: 'drawer', id }`) |
| `calendarInteraction.onDateClick.props` | object (truncated) |  |  | Per-trigger overrides applied to the referenced drawer component |
| `calendarInteraction.timeSlotInterval` | number |  |  | Time slot interval in minutes for week/day views (default: 60) |
| `calendarInteraction.showCurrentTimeIndicator` | boolean |  |  | Show a horizontal line at the current time in week/day views |

`dateField` supplies each event's start and `endDateField` its end, for events that span time. `labelField` is rendered as the event's label, and `defaultView` is `month`, `week` or `day`. `maxEventsPerDay` caps a day cell before it collapses into a "+N more" affordance.

`calendarEvent.onEventClick` runs when an event is clicked and `calendarInteraction.onDateClick` when an empty date or slot is. `calendarInteraction.timeSlotInterval` is the slot length in minutes for week and day views, and `showCurrentTimeIndicator` draws a line at the current time.

```yaml
tables:
  - name: bookings
    fields:
      - { name: customer_name, type: single-line-text }
      - { name: starts_at, type: datetime }
      - { name: ends_at, type: datetime }
pages:
  - name: Bookings
    path: /bookings
    components:
      - type: calendar
        dataSource: { table: bookings }
        dateField: starts_at
        endDateField: ends_at
        labelField: customer_name
        defaultView: week
        calendarInteraction: { timeSlotInterval: 30, showCurrentTimeIndicator: true }
```

## A coloured event is a block, not a dot

A calendar reads `colorField` at the top level — the record-view colour rules are the same ones a kanban board follows, including where the hue comes from and what happens when a value declares none.

In month view a calendar renders a timed event as a small dot by default. A dot is mostly empty space, so a colour applied to it is close to invisible. An event that carries a colour therefore renders as a filled **block** instead; an event with no colour keeps the default dot.

Expect that as a visible layout change on any month-view calendar that sets `colorField` — day cells that used to hold lines of dots now hold bands of filled blocks.

## Behaviour

### Data Calendar

- `type: 'calendar'` is a valid component type in page components
- `dataSource.table` binds the calendar to a table
- `dateField` maps a record's date/datetime field to calendar position
- `endDateField` enables multi-day events spanning across cells
- `defaultView` sets the initial view mode (month, week, day)
- Navigation buttons (previous, next, today) change the visible period
- `labelField` renders the record field value as the event label
- `colorField` maps field values to event background colors
- Multi-day events span across date cells from dateField to endDateField
- `maxEventsPerDay` limits visible events with a "+N more" overflow
- `onEventClick` triggers a navigation or modal action on event click
- `onDateClick` triggers a create action with the clicked date pre-filled
- Dragging an event to a different date updates the `dateField`
- Week/day views show time slots based on `timeSlotInterval`
- `showCurrentTimeIndicator` renders a line at the current time
- The week and day views render rather than throwing on a slot duration
- A timed view honours the slot interval and the now-indicator
- User can complete full calendar workflow end-to-end (regression)

### System read-endpoint data source

- A system-source calendar whose `dateField`/`labelField` are NOT app.tables fields boots successfully (field cross-validation is skipped)
- With `dataSource.system`, the calendar fetches `system.endpoint` and never falls through to `/api/tables/:t/records`
- The `{ items: [...] }` envelope is normalized to calendar events; `total` falls back to rows length when `totalKey` is absent
- `dateField` positions and `labelField` labels each event against endpoint rows (config drives rendering, not a table schema)
- `dataSource.system.query` static params are merged into the endpoint request
- A system-source calendar offers no record-write affordances (no drag-to-reschedule, no `onDateClick` create, no event edit)
- A system-source calendar offers no saved/user views and no user-preferences (read-only, endpoint-shaped)
- View switching (month/week/day) stays ON for a system source — a read-side control that re-reads the endpoint, never the records API
- The existing `dataSource.table` binding still positions table rows on the date grid unchanged (discriminated-union regression)
- A system-source calendar validates, fetches its endpoint, normalizes the envelope into events, gates writes off, keeps view switching, leaves DB intact
