
# The Form Component

> form — one type, two modes: table-bound writes through the records API, static submits to a declared form or to a URL of your own.

`dataSource` is what decides the mode. Declare one and the form is **table-bound**: its fields resolve against that table's columns, and submitting writes a record through the records API. Omit it and the form is **static**: it collects the fields declared on it and submits them to a form you declared in `forms[]` (`formRef`) or to a URL you name (`endpoint`). Nothing else about the component changes.

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `formRef` | string |  |  | Reference a top-level form by name (app.forms[].name). Renders that form inline. |
| `layout` | enum | `single-column`, `two-column`, `custom` |  | Form layout mode: single-column \| two-column \| custom |

### `dataSource`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `dataSource` | object |  |  | Binds a component to table data. Supports list, single-record, and search modes with filtering, sorting, and pagination. |
| `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'. |

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

### `wizard`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `wizard` | object |  |  | Multi-step wizard configuration. Splits form fields into sequential steps with Next/Back navigation. |
| `wizard.steps` | array |  |  | Ordered list of wizard steps |
| `wizard.steps[]` | object (truncated) |  |  | One step of the wizard: its label, and the fields it collects |

### `inlinePrefill`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `inlinePrefill` | object |  |  | Auto-prefill relationship/scalar fields on an embedded form using values from the host page record. |
| `inlinePrefill.prefill` | object |  |  | Map of form-field column name to prefill value. Supports `$parent.<field>` tokens that resolve against the host page record. |
| `inlinePrefill.lockPrefill` | boolean |  |  | When true, prefilled fields render as hidden inputs and the server revalidates the parent on submit (returns 422 if the parent is gone). |

### `endpoint`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `endpoint` | object |  |  | Custom-endpoint submit target for a form: POST collected field values as JSON to an arbitrary url, with response-envelope tolerance and the shipped onSuccess effects (status + sibling refetch). |
| `endpoint.url` | string |  |  | Custom submit URL (any path; not the records API). e.g. /api/auth/admin/create-user |
| `endpoint.method` | enum | `POST`, `PUT`, `PATCH` |  | HTTP method for the custom-endpoint submit (defaults to POST) |
| `endpoint.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) |
| `endpoint.submitLabel` | string |  |  | Submit button label (defaults to the form submit label) |
| `endpoint.submitVariant` | enum | `default`, `destructive`, `outline`, `secondary`, `ghost`, `link`, `fab` |  | Visual weight of the submit button, from the platform button vocabulary (the same members a `button` component accepts). Omit for the primary 'default' fill, unchanged. Set 'secondary' when a page stacks several small forms and a column of primary buttons would make every row look like the page's main action. |
| `endpoint.onSuccess` | object |  |  | Success handler for a fetch action: the toast slot plus optional client-state effects — a persistent inline status region (status), a sibling data-bound refetch (refetch), and a full-page reload (reload) that recomposes the page server-side. reload is mutually exclusive with status and refetch. |
| `endpoint.onSuccess.type` | enum | `toast` |  | What the component does once the action returns — navigate away, reset the form, show a message or a success page, send the reader to their role landing, or raise a toast. |
| `endpoint.onSuccess.message` | string |  |  | Toast notification message. Supports $variable references. |
| `endpoint.onSuccess.variant` | enum | `default`, `success`, `destructive`, `error`, `warning`, `info` |  | Visual style of the toast notification |
| `endpoint.onSuccess.duration` | number |  |  | Auto-dismiss duration in milliseconds (default: 5000) |
| `endpoint.onSuccess.actionLabel` | string |  |  | Label of an optional action button rendered inside the toast |
| `endpoint.onSuccess.actionUrl` | string |  |  | URL invoked (POST) when the toast action button is clicked. Required with actionLabel. |
| `endpoint.onSuccess.status` | object (truncated) |  |  | A persistent inline role="status" region populated on success (the persistent counterpart to a transient toast). |
| `endpoint.onSuccess.refetch` | string \| array (truncated) |  |  | props.id (or array of ids) of sibling data-bound component(s) to re-query on success. Works for both a DB-table dataSource and a dataSource.system read endpoint. |
| `endpoint.onSuccess.reload` | boolean |  |  | When true, the browser reloads the page after a successful request so the SERVER recomposes it — the effect refetch cannot express, because refetch re-queries one region and skips any region holding a mounted island. Use it when the request changes something the server read at render time (the active language, the chrome). Refused at decode alongside status or refetch (both are same-page effects the reload subsumes); the required toast message is NOT displayed, because the reload replaces the document that would have shown it. |
| `endpoint.onError` | object |  |  | Toast notification rendered after a fetch action completes |
| `endpoint.onError.type` | enum | `toast` |  | What the component does once the action returns — navigate away, reset the form, show a message or a success page, send the reader to their role landing, or raise a toast. |
| `endpoint.onError.message` | string |  |  | Toast notification message. Supports $variable references. |
| `endpoint.onError.variant` | enum | `default`, `success`, `destructive`, `error`, `warning`, `info` |  | Visual style of the toast notification |
| `endpoint.onError.duration` | number |  |  | Auto-dismiss duration in milliseconds (default: 5000) |
| `endpoint.onError.actionLabel` | string |  |  | Label of an optional action button rendered inside the toast |
| `endpoint.onError.actionUrl` | string |  |  | URL invoked (POST) when the toast action button is clicked. Required with actionLabel. |

### `fields`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `fields` | array |  |  | Per-field configuration for form component (labels, placeholders, visibility) |
| `fields[]` | object |  |  | Per-field configuration for a form component |
| `fields[].field` | string |  |  | Field identifier: a table column name (table-bound form) OR the JSON body key (endpoint-bound form) |
| `fields[].control` | enum | `text`, `email`, `password`, `number`, `tel`, `url`, `textarea`, `select` |  | Explicit input control for an endpoint-bound form field (text/email/password/number/tel/url/textarea/select). Omitted for table-bound forms (control derived from the column type). |
| `fields[].options` | array (truncated) |  |  | Dropdown options for a control: select field ({ value, label? }) |
| `fields[].optionsSource` | object (truncated) |  |  | Dynamic option source for a choice control: table rows, or the rows of a system read endpoint |
| `fields[].label` | string |  |  | Custom label text (overrides default field name) |
| `fields[].description` | string |  |  | Guidance text rendered beside the control and linked via aria-describedby (overrides the bound field's description). Required to describe a control on an endpoint-bound form, which has no table field schema to resolve from. Unlike a placeholder it persists once the user starts typing. |
| `fields[].placeholder` | string |  |  | Placeholder text shown when field is empty |
| `fields[].readOnly` | boolean |  |  | If true, field is displayed but not editable |
| `fields[].disabled` | boolean |  |  | If true, field input is disabled |
| `fields[].defaultValue` | string \| number \| boolean |  |  | Default value for create mode. Supports static values or $variable references. |
| `fields[].hidden` | boolean |  |  | If true, field value is submitted but input is not rendered |
| `fields[].visibleWhen` | object (truncated) |  |  | Condition supporting simple comparisons and compound OR/AND logic for field visibility, required, and disabled states |
| `fields[].requiredWhen` | object (truncated) |  |  | Condition supporting simple comparisons and compound OR/AND logic for field visibility, required, and disabled states |
| `fields[].disabledWhen` | object (truncated) |  |  | Condition supporting simple comparisons and compound OR/AND logic for field visibility, required, and disabled states |
| `fields[].accept` | string |  |  | Comma-separated MIME types or extensions (e.g. "image/*,.pdf") |
| `fields[].dropZone` | boolean |  |  | If true, renders a drag-and-drop area for file uploads |
| `fields[].maxFiles` | number |  |  | Maximum number of files allowed (for multiple-attachments fields) |

### `fieldGroups`

| Path | Kind | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `fieldGroups` | array |  |  | Groups form fields under labeled section dividers |
| `fieldGroups[]` | object |  |  | Groups form fields under a labeled section divider |
| `fieldGroups[].label` | string |  |  | Group label displayed as a section divider above the fields |
| `fieldGroups[].fields` | array (truncated) |  |  | Array of field names belonging to this group |

`dataSource` is table-only by design, since writes go to a table and never to a read endpoint; `mode: single` supplies current values for an edit form. `layout` is `single-column` by default, with `two-column` and `custom` beside it. `fieldGroups` divides the form into `{ label, fields }` sections.

```yaml
tables:
  - name: contacts
    fields:
      - { name: email, type: email }
      - { name: notes, type: long-text }
pages:
  - name: Contact
    path: /contacts/:id
    components:
      - type: form
        dataSource: { table: contacts, mode: single, param: id }
        layout: two-column
        action: { type: crud, operation: update, table: contacts }
        fields:
          - { field: email, label: 'Email address' }
          - { field: notes, control: textarea }
```

## Submitting to your own endpoint

`endpoint` is the third submit target, beside a table and a `forms[]` entry: the form collects its declared fields and POSTs them as a JSON body — `{ [field]: value }` — to any URL you name. Nothing goes through the records API, so the destination can be a platform route, an admin endpoint, or something of your own. Each field must then name its own `control`.

`url` is required and takes any path or fully-qualified URL. `method` is `POST` by default, or `PUT` or `PATCH`. `responseEnvelope` decides how the response body is read when judging success or failure, and is `sovrium` by default. `submitLabel` overrides the button text, `onSuccess` runs on a 2xx — a toast, plus the client-state effects `status`, `refetch` and `reload` — and `onError` shows a toast when the submit fails.

**`submitVariant` is for a page that stacks several forms.** One form whose submit _is_ the page's main action wants the primary fill, and gets it by declaring nothing. A settings page drawing six one-row forms down a column gets six primary buttons instead, none of which is the main action — so each declares a quieter weight and the page regains a single focal point. The vocabulary is the `button` component's own: `default`, `destructive`, `outline`, `secondary`, `ghost`, `link`, `fab`.

```yaml
- type: form
  endpoint:
    url: /api/account/display-name
    method: POST
    submitLabel: Save
    submitVariant: secondary
    onSuccess: { type: toast, variant: success, message: Name saved }
    onError: { type: toast, variant: destructive, message: Could not save the name }
  fields:
    - { field: name, control: text, label: Display name }
```

The member list is resolved through the same recipe the button component uses, so a submit and a standalone button asking for `secondary` cannot drift apart.

## Prefilling an endpoint form

An endpoint-bound field carries its own `defaultValue`. Nothing derives it from a column — there is no table binding — so it is the only way such a form opens on anything but empty controls. A static value fills a text control, and on a `select` it is the option that arrives already chosen.

A `defaultValue` naming `$session.<field>` is the **caller's own** value, and it is filled in the browser rather than during rendering:

```yaml
- type: form
  endpoint: { url: /api/invitations, method: POST, submitLabel: Send the invitation }
  fields:
    - { field: role, control: text, label: Role, defaultValue: member }
    - { field: invitedBy, control: text, label: Invited by, defaultValue: $session.name }
    - field: locale
      control: select
      label: Language
      defaultValue: fr
      options:
        - { value: en, label: English }
        - { value: fr, label: 'Français' }
```

**Why the identity is not resolved on the server.** A page is composed once and may be cached, so resolving `$session.name` while rendering would write whoever requested it first into every copy handed out afterwards — one reader's name arriving in the next reader's form. The served bytes therefore name nobody: the server emits the template and the browser fills it against the caller's own session. An anonymous visitor gets an empty control, never the literal `$session.name`, and the static defaults beside it are unaffected, since they are the same for every reader.

The resolvable fields are `email`, `name`, `role` and `id` — the same set session-bound text resolves, through the same mechanism.

## It is the one type a specimen may not draw

`form` is excluded from the design-system catalogue. It emits a submit control unconditionally, in both its create and its update branch, and a preview frame may carry no write path — so the catalogue reports the type and its reason rather than drawing it. That is a safety rule rather than a gap in the kit.

## Behaviour

### Authentication Page Components

- `action type auth method login strategy email` renders login form
- Login form validates email and password before submission
- Invalid credentials display error message
- Successful login navigates to `onSuccess` navigate path and creates session in database
- Login form requires auth strategies to include email
- `action type auth method signup strategy email` renders signup form
- Signup form validates email and password before submission
- Successful signup creates a new user account (verified in `auth.user` table) and redirects
- Password requirements are validated on signup
- Signup form requires auth strategies to include email
- OAuth control starts Better Auth social sign-in with a POST
- `onSuccess` navigate rides the social sign-in as its `callbackURL`
- OAuth login requires matching provider in auth strategies
- `visibility.when: authenticated` shows component only to logged-in users
- `visibility.when: unauthenticated` shows component only to guests
- `roles: [admin]` restricts visibility to admin role
- Multiple roles use OR logic for visibility
- `when` and `roles` combine with AND logic
- `method: resetPassword` renders password reset request form
- Submitting the form sends a password reset email and creates verification record in database
- Password reset validates token, allows setting new password, and persists change in database
- `action type auth method logout` signs the session out (and gates protected pages afterward)
- OAuth sign-in renders one painted button, never a nested control
- User can complete full auth-components workflow (regression)
- Logout button ends the session and gates protected pages (regression)

### Create Record Form

- `action type crud` with `operation create` renders a form
- Form fields are generated from table schema definition
- Required fields validated before submission
- Successful creation triggers `onSuccess` redirect and persists record in database
- Create operation requires create permission
- `create: 'all'` renders the form for anonymous AND signed-in callers
- An admin sees a create form whose role allowlist omits `admin`
- A `status` field left untouched creates the record with its declared `default`
- A `relationship` field left untouched is stored as NULL, not an empty string
- A `multi-select` field left untouched is stored as NULL, not an empty string
- A `progress` field left untouched is stored as NULL, not an empty string
- Every constrained field type left empty is omitted from the create payload
- SSR skeleton renders a `status` field as a `select` with its options and default
- Hydrated form renders a `status` field as a combobox with its options and default
- A `barcode` field is omitted when empty only if its own config constrains it
- User can complete full create record workflow (regression)
- User can create a record leaving every constrained field empty (regression)
- User creates a record whose barcode fields differ only in config (regression)

### Update Record Form

- `operation update` renders form pre-filled with existing data
- Form fields reflect current record values from data source
- Modified fields validated before submission
- Update operation requires update permission
- `update: 'authenticated'` renders the form for every signed-in role, hidden for anonymous
- Successful update persists changes to database
- Successful update triggers `onSuccess` redirect
- User can complete full update record workflow (regression)

### Delete with Confirmation

- `operation delete` triggers delete action
- `confirm: true` displays confirmation dialog before delete
- Delete uses soft-delete by default (verified by `deleted_at` timestamp in database)
- Successful deletion triggers `onSuccess` handler (with database confirmation of soft-delete)
- Delete operation requires delete permission
- User can complete full delete workflow (regression)

### Form Validation Feedback

- Inline field errors displayed next to invalid field
- Summary error display shows all errors at top of form
- Server-side validation errors mapped to form fields
- User can complete full validation workflow (regression)
- Server-rejected values are marked on the field they belong to (regression)

### CRUD with Attachment Fields

- Create form with `single-attachment` field uploads file and stores reference in record
- Create form with `multiple-attachments` field uploads files and stores array in record
- Update form pre-fills attachment field with existing file (shows filename/thumbnail)
- Update form allows replacing an existing single attachment with a new file
- Update form allows removing an existing attachment (sets field to null)
- Update form allows adding/removing individual files in a multiple-attachments field
- Delete record with attachments cleans up stored files from storage backend
- User can complete full CRUD with attachments workflow (regression)

### Configurable, localizable CRUD form labels

- Action `submitLabel` overrides the built-in "Create" submit-button text
- Action `fields[]` override the table-derived field labels and placeholders
- `submitLabel` + field labels localize through page `meta.lang` + app `languages`
- Full configurable + localized CRUD-form label workflow

### Auto-Generated Form from Table

- `type: form` with `dataSource.table` auto-generates fields from table schema
- Each table field type maps to appropriate input (text->input, select->dropdown, date->datepicker, richtext->editor, number->number input, boolean->checkbox, single-attachment->file upload, multiple-attachments->multi-file upload)
- `fields` array includes/excludes/reorders specific fields
- Required table fields show required indicator and validate before submit
- `dataSource.mode: single` pre-fills form with existing record (edit mode)
- `action.type: crud` with `operation: create` or `update` determines form behavior
- User can complete full auto-generated form workflow (regression)

### Field Configuration

- Per-field `label` overrides default field name
- Per-field `placeholder` shows hint text
- Per-field `readOnly: true` renders non-editable display
- Per-field `defaultValue` (static or `$variable`) pre-fills on create
- Per-field `hidden: true` submits value without rendering input
- Per-field `description` renders help text linked by `aria-describedby`, overriding the bound field's `description`
- User can complete full field configuration workflow (regression)

### Conditional Fields

- `visibleWhen` condition shows/hides field based on another field's value
- Condition operators: `eq`, `neq`, `contains`, `empty`, `notEmpty`
- Hidden fields excluded from validation
- Multiple conditions on same field evaluated with AND logic
- User can complete full conditional fields workflow (regression)

### Form Layout

- `layout: single-column` stacks fields vertically (default)
- `layout: two-column` renders fields in 2-column responsive grid
- `layout: custom` allows wrapping fields in `children` sections
- `fieldGroups` groups fields with label dividers
- A form NESTED IN A CARD still lays out as the column it declares, and its submit is separated from the last field by a real distance — measured as computed style and geometry, never asserted by class, because an inline `display` from `buildEmptyElementStyles` beats every display utility and leaves the classes painting nothing
- User can complete full form layout workflow (regression)

### Form Actions

- `action.type: crud` creates/updates record
- `action.type: automation` triggers automation with form data as inputData
- Combined: CRUD action first, then automation receives created record ID
- Submit button `props.label` and `props.variant` customizable
- User can complete full form actions workflow (regression)

### Validation & Feedback

- Inline field-level error messages on blur and submit
- Summary error banner at form top listing all errors
- Success: onSuccess toast + navigate triggered after successful submit
- User can complete full validation workflow (regression)

### File Upload Fields

- `single-attachment` field type renders a file upload input (file picker button)
- `multiple-attachments` field type renders a multi-file upload input (supports selecting multiple files)
- `accept` prop restricts selectable file types in the browser file dialog (e.g., `image/*`, `.pdf`)
- File size validation rejects files exceeding `maxFileSize` from table field definition
- File type validation rejects files not matching `allowedFileTypes` from table field definition
- `dropZone: true` renders a drag-and-drop area for file selection
- Upload progress indicator shown while file is uploading
- Image files display a thumbnail preview after selection; non-image files display filename and size
- Selected files can be removed before form submission
- Multi-file input enforces `maxFiles` limit from table field definition
- Edit mode pre-fills file inputs with existing attachment data (filename, thumbnail) and allows replace/remove
- File is uploaded (to storage backend) and reference stored in record on form submit
- Validation error shown when required attachment field has no file selected
- The upload is POSTed to the bucket declared on the bound column, and the preview URL names that same bucket
- Edit mode renders an existing stored attachment against the column's declared bucket
- A file chosen before the upload island mounts is kept: the filename previews and the upload POSTs exactly once
- Text typed before a tab-panel form mounts survives and is submitted — the visible input and the saved row both carry what was entered, not what the page loaded with
- User can complete full file upload form workflow (regression)
- Uploads and previews honour the column's declared bucket end-to-end (regression)

### File Upload with Automation

- `$form.{field}` references for attachment fields resolve to file metadata object (url, name, size, mimeType)
- `$form.{field}` for `multiple-attachments` resolves to array of file metadata objects
- Automation `inputData` can reference uploaded file via `$form.file` and receive file metadata
- Combined CRUD + automation: record created with attachment, then automation receives record with file URL
- User can complete full file upload with automation workflow (regression)

### Form Success Page

- `onSuccess.type: successPage` replaces the form with success page content after successful submission
- Success page with `redirect` navigates to the specified URL after a 2-second delay (supports `$record.id` variables)
- `showSummary: true` displays a read-only summary of the submitted field values on the success page
- Success page `actions` with `action: reset` resets the form to initial state for a new submission
- Success page renders a checkmark icon, the configured title, message, and any action buttons — and the actions are painted rather than transparent, the navigate link reading as the quieter of the pair
- User can complete full form success page workflow (regression)

### Form Reset After Success

- `onSuccess.type: reset` clears all fields to their default values and shows a success toast after submission
- `preserveFields` array retains specified field values while clearing all others on reset
- Multi-step wizard form resets to step 1 when `onSuccess.type: reset` is triggered
- The `onSuccess.type: reset` schema on the in-page form component is parity-compatible with the top-level form schema — same `preserveFields` semantics, same wizard reset behavior
- User can complete full form reset after success workflow (regression)

### Advanced Conditional Fields

- visibleWhen with or array shows field when ANY condition matches
- nested and/or conditions evaluate correctly
- requiredWhen condition makes field required only when condition is met
- requiredWhen unmet allows form to submit without that field
- disabledWhen condition renders field as disabled when met
- disabled field becomes editable again when disabledWhen no longer met
- multiple fields depending on same source field all update when source changes
- hidden conditional field has its value excluded from submission payload
- visibleWhen references field in previous wizard step
- layout reflows when conditional field becomes hidden in grid

### Custom-Endpoint Submit

- An endpoint-bound form renders its explicit-`control` fields (typed inputs + a `select`) and the custom submit button
- Submitting the form POSTs the custom endpoint with the collected field values as a JSON body (not the records API)
- `onSuccess.refetch` re-reads the sibling `dataSource.system` grid so the new row appears without a reload
- An endpoint-bound field honours `defaultValue`, so the control arrives carrying what it is about to change rather than an empty box
- A `defaultValue` of `$session.<field>` ships as a TEMPLATE and is filled CLIENT-side, so the served bytes name nobody and a cached copy cannot carry one reader's identity to the next
- An anonymous visitor gets an empty control, never the literal token
- `endpoint.submitVariant` paints the submit in the platform button vocabulary's own words, and omitting it leaves the submit exactly as it is today
- A `select` whose `$session.<field>` default resolves to nothing keeps its authored option instead of going blank, while one that resolves still moves to its value
- `endpoint.onSuccess.reload` makes the SERVER recompose the page, and a form without it re-reads nothing
- User can render explicit controls, POST the custom endpoint, and refetch the sibling system grid (regression, dual-dialect)

### Reference Top-Level Form

- The `type: 'form'` component accepts a `formRef: <name>` field that references `app.forms[].name`; an unknown name fails app schema validation with an error naming the page, the component, and the missing form
- When `formRef` is set, the component inherits ALL behavior from the referenced top-level form: `fields[]`, layout, `steps[]` (multi-step), conditional logic, attachment fields, `onSuccess`, and `onError`
- `formRef` is mutually exclusive with each of `dataSource`, `fields`, and `fieldGroups`; setting any of those alongside `formRef` fails validation with a single error citing the conflict
- When `formRef` is set, the only honored component-level overrides are display-layer props: `props.label` (submit button), `props.variant` (button variant), `responsive`, and `visibility`; component-level `layout` and `action` props are ignored with a single warning at validation time
- The host page's access rules (e.g. `pages[].access: authenticated`) apply when the form is rendered through `formRef`; the same top-level form remains independently reachable at `/forms/{name}` per its own (default-public) access rules
- Submitting the embedded form follows the top-level form's `submitTo` and `onSuccess`/`onError` exactly as if the user had submitted at `/forms/{name}` (one shared submission ledger row, one bound table write, one bound automation invocation)

### Inline Multi-Step Wizard Layout

- `wizard.steps[]` requires `minItems: 1`; an empty or missing `steps[]` array fails validation with a clear error naming the form component
- Each step's `fields[]` references field names that exist on the inline form's `fields[]` (or, when fields are auto-generated, on the bound table's columns); unknown names fail validation
- The renderer displays one step at a time with a progress indicator showing current step label and total step count; Next and Back buttons navigate between steps
- Next-button click validates the current step's required+visible fields; failing validation blocks advancement and surfaces inline errors on the current step
- Submit fires only on the final step; the request body includes values from all steps (skipped fields are omitted)
- The `wizard:` schema accepted on the form component is identical whether the component appears under `pages[].components[]`, top-level `app.components[]`, or nested inside another component's `children[]`
- Setting `wizard:` and `formRef:` on the same component fails validation with an error explaining that wizard layout flows from the referenced form's `layout: multi-step` definition instead
- The wizard Back, Next and Submit controls are painted rather than transparent, and Back reads as the quieter of the pair
- User can complete full inline-form wizard workflow (regression)
