Skip to content

Commit 55828c5

Browse files
feat(spec): an empty state names its one next step (link: { label, url }), and a list page carries its own emptyState (#23098)
Fixes #23004 Fixes #23005 Clause-②: yes (widening: two new accepted keys) The maintainer's "one plain sentence and one action" for an empty page (objectstack-ai/cloud#2776, route letter A on objectstack-ai/cloud#2803, contract first), as one fold of two cards (claim `6129995570`, member claim `6130003856`). One commit per card, then the generated artifacts and the changeset. ## What **#23004 — `EmptyStateSchema.link`** (`packages/spec/src/ui/view.zod.ts`, commit `ec742e938`) - An empty state takes one optional, closed `link`: `{ label, url }`, both required and nothing else. `label` is an `I18nLabel` (a string or an inline locale map). A link only: the empty state invokes no action, flow or script. - `url` is judged by `navigationTargetRefusal` from `navigation-target.ts`, the function a `type: 'url'` action's `target` is judged by. It is reached by relative import, so there is no copy and no second predicate. The subject is "An empty-state link's `url`". An empty `url` is refused once, by `.min(1)` ("An empty-state link's `url` is empty."), and the superRefine skips the empty string so the author reads one issue. - `EMPTY_STATE_ACTION_GUIDANCE` is restated. `action` / `button` now get the link (`link` with `{ label, url }`, "It runs no action, flow or script.") and keep the `addRecord` pointer. A flat `url` / `href` on the empty state gets "`link: { label, url }`". The link carries aliases `href`/`to`/`target` → `url` and `text`/`title` → `label`. - The `link` describe carries `[PLANNED — not yet rendered]`. The view ledger's `emptyState` row note records the exception. **#23005 — `InterfacePageConfigSchema.emptyState`** (`packages/spec/src/ui/page.zod.ts`, commit `477cedee5`) - A `type: 'list'` page takes `interfaceConfig.emptyState: EmptyStateSchema.optional()`, by reference: the list view's own declaration, with no second shape. Its describe carries `[PLANNED — not yet rendered]`, and the page ledger's `interfaceConfig` row note records the exception. ## Measured calls (the dispatch's mechanism assumptions) 1. **Every door that carries `EmptyStateSchema`, on `origin/main`.** `git grep EmptyStateSchema` finds two references: `ListViewSchema.emptyState` (`view.zod.ts`) and `ComponentPropsMap['object-grid'].emptyState` (`component.zod.ts`). This PR adds a third, `InterfacePageConfigSchema.emptyState`. The build's dropped-refinement census gives the published reach: 18 schemas, listed under JSON Schema below. The translation group's `_views.VIEW.emptyState` is a separate `{ title, message }` string shape, so it is unaffected. Each door against objectui `origin/main` `7af0ab3` (read-only `git show`): | door | drawn today | the link / the page's own key | |---|---|---| | list view `emptyState` (and `listViews`, view documents, overlays) | `title`/`message`/`icon`: `ListView.tsx:5688-5696` → `ListEmptyState` | not read. Planned, carrier objectstack-ai/objectui#12270 item 1 | | `object-grid` page block `emptyState` | `title`/`message`/`icon`: `ObjectGrid.tsx:6647-6655` → `DataEmptyState` | not read. **No carrier names it**: objectstack-ai/objectui#12270 lists `ListView.tsx` and `InterfaceListPage.tsx` only (see Acceptance notes) | | list page `interfaceConfig.emptyState` (new) | none: `InterfaceListPage.tsx:503` passes the source view's `emptyState: view.emptyState` | planned, carrier objectstack-ai/objectui#12270 item 2 | Zero readers: in objectui at `7af0ab3`, `git grep` for an `emptyState` `.link` read, an `emptyState.action` read or an `interfaceConfig` `emptyState` read finds 0 real hits. The 1 hit is `emptyStateHint`, a different key. Control: `emptyState` itself hits 5 / 3 / 1 times in `ListView.tsx` / `ObjectGrid.tsx` / `InterfaceListPage.tsx`. 2. **The key name and the guidance.** A `guidance` entry is consulted only for an undeclared key, and `alias-integrity.test.ts` holds every entry to a key the shape refuses. A declared `action` key would have had to drop its own guidance. So the key is `link`, as the consumer wrote it ("a link (`label` + `url`)"), and `action` / `button` stay guidance entries, restated to point at it. 3. **Liveness: which mechanism.** It is the describe marker plus the row's own `note`, the #23079 precedent. A gate-read `planned` row is not possible without drilling. `view/list.emptyState` and `page/interfaceConfig` are both recorded undrilled containers (`undrilled-containers.baseline.json`), so the walk never reaches `link` or `interfaceConfig.emptyState`, and a marker there is not read either. Drilling `view/list.emptyState` would take four child rows (three `live` with the objectui evidence above, `link` `planned`) and one baseline shrink. That is outside the claimed file surface, so it is noted below and not done. `check:liveness` exits 0. 4. **`check:empty-state`** is the pure source audit for permissive-empty prose. It read the new describes and guidance and exits 0. ## JSON Schema: the link's grammar is a dropped refinement `navigationTargetRefusal` is a function, so the rule is a `superRefine`, and the published JSON Schema cannot express it. `link.url` publishes as `type: string, minLength: 1`, and the position is named under `x-dropped-refinements`. `dropped-refinements.baseline.json` gains the sites exactly as the build printed them: 18 sites, with `ui/EmptyState` a new entry (`link.url`), and one more site under each of `ui/ListView`, `ui/ObjectListView`, `ui/View`, `ui/ViewItem`, `ui/ViewItemWire`, `ui/ObjectGridProps`, `ui/InterfacePageConfig`, `ui/Page`, `data/Object`, three `system/*Operation`/`ChangeSet` and five `api/*Response`/package schemas. The `measured` counts stack with main's #23062 (+1 schema, +2 sites): 228 schemas and 731 sites. The build checks the sites, and it exits 0 at the head. A regex copy that would project as `pattern` would be the second grammar the triage direction rules out. ## Other files the change reaches (outside the claimed file surface: stated, not silent) - `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`: one `omit` ledger row for `page` `interfaceConfig.emptyState`. The gate measured it: with the key added, "page.interfaceConfig: accepted by the Zod but unauthorable in the form — offer it, or add a ledger entry: expected [ 'emptyState' ]". The row is an omit, not a form control, because no console list page draws the key yet, and a Studio control would let an author write a value nothing draws. The row's reason says it leaves with the renderer half. - `packages/spec/dropped-refinements.baseline.json` (above), and the generated artifacts `check:generated` proved stale: `authorable-surface/ui.json` (`ui/EmptyState:link`, `ui/InterfacePageConfig:emptyState`), `content/docs/references/{ui/view,ui/page,ui/component,data/object,api/protocol}.mdx`, and `docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md` (one more strict site). - No `api-surface/` change: the link schema is module-private (`emptyStateLinkSchema`), so no new export. ## Readers of the two schemas: updated or unaffected - Types: `EmptyState` (`z.input`) gains `link?: { label; url }`, and `InterfacePageConfig` / `InterfacePageConfigParsed` gain `emptyState?`. The superRefine adds no transform, so the `type-alias-convention.pin.test.ts` isomorphism holds. - `@objectstack/cli` `i18n-extract.ts` emits `_views.VIEW.emptyState.{title,message}` and is unaffected (see Acceptance notes for `link.label`). - `@objectstack/lint` (`showcase-shape.fixtures.ts`, the page walkers), `@objectstack/platform-objects` and `examples/app-showcase` author `emptyState` with `title`/`message`/`icon`, or list pages without it. All are unaffected, and their suites ran below. ## Verification All commands ran under `scripts/pm/os-verify-lock.sh`, with exit codes captured before any pipe. The final head is `eb4d7de12`, which is this branch with `origin/main` `6662e3f59` merged in. Two merges were made through `scripts/pm/os-regen-merge.sh`. The first brought #23080, and its two driver-dropped artifacts were regenerated in `a449f542c`. The second brought #23062, and its `dropped-refinements.baseline.json` conflict was settled in `eb4d7de12` by stacking the counts. The net delta against `origin/main` is still these 16 files, +604/−25. - **Spec build and generated artifacts at `eb4d7de12`:** `pnpm --filter @objectstack/spec build` exit 0. `check:generated` exit 0: "All 14 generated artifacts are up to date". - **New pins** (84 tests, all passing): - `src/ui/empty-state-link.test.ts` (61): §1 accepted forms, byte-identical. §2 one refusal per grammar class, first sentence + path `link.url` + code `custom`, and the empty url as `too_small`. §3 one grammar: on 27 targets the link's verdict and reason equal a registered `type: 'url'` action's, subject set aside. §4 malformed links: no label → `invalid_union` at `link.label`; no url / numeric url / bare string / array → `invalid_type`; extra key / an `action` on the link → `unrecognized_keys` at `link`. §5 guidance. §6 the list-view and `object-grid` doors answer what the schema answers. - `src/ui/page-interface-empty-state.test.ts` (23): accepted on `InterfacePageConfigSchema` and through a whole `type: 'list'` page. Refused by code + path, at `emptyState…` and at `interfaceConfig.emptyState…`. A lit unknown-key control on the config itself. Identity: the member unwraps to `EmptyStateSchema._zod.def`, plus a corpus parity check. - **Targeted spec files at `eb4d7de12`:** the two pin files plus `metadata-form-zod-reconciliation`, `alias-integrity`, `component-object-grid-declared-keys.pin`, `view-strictness-batch18`, `type-alias-convention.pin`, `page`, `action-url-target-scheme-set`. 9 files, 474 passed. - **Full spec suite at `a449f542c`**, before the second merge, whose incoming commits touch `src/automation/**` and the baseline counts only: `pnpm --filter @objectstack/spec test` 659 files, 19950 passed. `pnpm --filter @objectstack/spec typecheck` exit 0, including `check:test-typecheck` ("52 file(s) / 246 error(s) / 135 pinned signature(s) held", so the new test files compile with no new debt). At `5bbc19059`, before both merges: 658 files, 19933 passed. - **Consumer suites at `5bbc19059`** (the same net diff, older base): - `@objectstack/lint` 137 files, 6569 passed. - `@objectstack/platform-objects` 70 files, 1096 passed. - `@objectstack/example-showcase` 33 files, 409 passed. - `@objectstack/cli` `vitest run --project unit` 284 files, 4276 passed. - The cli `integration` tier is declared to CI. The diff touches no spawn entry. - **Reverse type check against the rebuilt `dist/ui/index.d.mts`.** A scratch program typed `EmptyState` / `InterfacePageConfig`. With `link: { label, url, openIn }` it exits 2 with `TS2353: Object literal may only specify known properties, and 'openIn' does not exist in type '{ label: …; url: string; }'`. With that line removed it exits 0, with a list-view link and a page `emptyState` link both compiling. - **Gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived 110 families at `eb4d7de12`, against merge base `6662e3f59`: the same list as at `5bbc19059`. All 110 were run at `eb4d7de12`. 109 exited 0 and 1 exited 1. `--ran` reconciliation: "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN". The red one is `pnpm check:platform-checklist`: 8 `ABSENT SYMBOL` / anchor-floor problems in `docs/qa/platform-checklist/areas/{access-security,attachments-storage,search}.json`, which this diff does not touch. Control leg: the same command in a clean detached worktree at `origin/main` `6662e3f59` exits 1 with the identical 8 lines (`diff` empty), as it also did at `8c1077615`. In the first battery at `5bbc19059`, `check:skill-examples` and `check:dual-build-cjs-loads` refused with exit 3 (unbuilt `client-react` and 8 other packages). They exited 0 after those packages were built, and both exited 0 in the battery at `eb4d7de12`. - **Control bytes:** `grep -naP` for raw control bytes over the 16 changed files finds none. The tab / line feed / carriage-return cases are escapes in source. `pnpm check:nul-bytes` exit 0. ## Acceptance notes Recorded here, not filed: - **The `object-grid` door accepts `link`, and no renderer card names it.** objectstack-ai/objectui#12270 lists `ListView.tsx` (item 1) and `InterfaceListPage.tsx` (item 2). `ObjectGrid.tsx:6647-6655`, the `object-grid` page block's own empty state, is not on it. Carrier: the seat, by adding it to objectstack-ai/objectui#12270's scope. - **The `object-grid` `emptyState` describe still reads `{ title, message, icon }`** (`component.zod.ts`). It is not wrong about what the grid draws, but it lists three of four accepted members. `component.zod.ts` belonged to #23080 in this round, so it is untouched here. Carrier: the next PR that touches that describe, or the objectstack-ai/objectui#12270 follow-up when the grid draws the link. - **`link.label` has no translation-bundle address.** `_views.VIEW.emptyState` declares `title` / `message` only, and `os i18n extract` emits those two only. The inline locale map is the label's route, and the page-prose route the translation file already names. Carrier: none. - **The `${…}` arm.** The grammar admits a target that opens with `${`, judged by the runner after interpolation, because the url action's set includes it. That holds only if the renderer opens the link through the action runner, which objectstack-ai/objectui#12270 item 1 already asks. Carrier: objectstack-ai/objectui#12270. - **When the renderer half lands:** the two `[PLANNED — not yet rendered]` markers leave, the two ledger notes are rewritten, `view/list.emptyState` can be drilled into a gate-read row, and the page form offers `emptyState` with the reconciliation `omit` row deleted. --- _Generated by [Claude Code](https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent abf8655 commit 55828c5

16 files changed

Lines changed: 604 additions & 25 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): an empty state names its one next step — `emptyState.link: { label, url }` — and a list page carries its own `interfaceConfig.emptyState`
6+
7+
Clause-②: yes (widening: two new accepted keys)
8+
9+
- **`EmptyStateSchema.link`.** An empty state takes one optional link beside `title`, `message` and `icon`: `{ label, url }`, both required and nothing else, for example `link: { label: 'How to publish a package', url: '/docs/publish-a-package' }`. It opens a page or an address and runs no action, flow or script. The shape is closed: a missing `label` or `url`, any other key and a non-object are refused at parse.
10+
- **The targets a `type: 'url'` action opens, and no others.** `link.url` admits an in-app path that starts with `/` or `./`, an address that opens with `http://`, `https://` or `mailto:`, and a template that opens with `${…}` (such as `${ctx.origin}/docs`), which is judged when it is clicked. Every other scheme (`javascript:`, `data:`, `tel:`, …), a protocol-relative `//host`, `/` followed by a backslash, a tab, line feed or carriage return, and a bare relative target such as `docs/x` are refused at `link.url` with the reason and the fix, in the same sentences a url action's `target` is refused with. An empty `url` is refused as empty.
11+
- **Every door that carries the empty state takes the link:** a list view's `emptyState` (and so an object's `listViews` and a view document's), the `object-grid` page block's `emptyState`, and a list page's new `interfaceConfig.emptyState`.
12+
- **`interfaceConfig.emptyState`.** A `type: 'list'` page takes its own empty state, the list view's shape itself: `{ title, message, icon, link }`, refused at parse exactly as the list view's is.
13+
- **Guidance.** `action` or `button` written on an empty state is now answered with the link, and still with the list view's `addRecord` block for an add-record entry point; a `url` or `href` written flat on the empty state is pointed into `link`.
14+
- **Not rendered yet.** No console renderer draws the link or a list page's own empty state today. Both parse and are not drawn, and their schema descriptions say so (`[PLANNED — not yet rendered]`). Until the console reads it, a list page still draws its source view's empty state, if any.
15+
- **Published JSON Schema.** `link.url` publishes as a string with `minLength: 1`. The target rule is a parse-time refinement the published file does not express; the file names its position under `x-dropped-refinements`.

‎content/docs/references/api/protocol.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,7 +1731,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
17311731
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
17321732
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
17331733
| **allowPrinting** | `boolean` | optional | Allow users to print the view |
1734-
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string }` | optional | Empty state configuration when no records found |
1734+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | Empty state configuration when no records found |
17351735
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view |
17361736
| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
17371737
| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -1817,7 +1817,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
18171817
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
18181818
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
18191819
| **allowPrinting** | `boolean` | optional | Allow users to print the view |
1820-
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string }` | optional | Empty state configuration when no records found |
1820+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | Empty state configuration when no records found |
18211821
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view |
18221822
| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
18231823
| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ const result = ApiMethod.parse(data);
410410
| **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration |
411411
| **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list |
412412
| **allowPrinting** | `boolean` | optional | Allow users to print the view |
413-
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string }` | optional | Empty state configuration when no records found |
413+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | Empty state configuration when no records found |
414414
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view |
415415
| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
416416
| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

‎content/docs/references/ui/component.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ Sort field and direction pair
793793
| **label** | `string \| Record<string, string>` | optional | Grid label — used as the table caption and export file title |
794794
| **title** | `string \| Record<string, string>` | optional | Fallback for `label` (the renderer reads `label \|\| title`) |
795795
| **description** | `string \| Record<string, string>` | optional | One line of help text drawn above the grid's rows — a string, or an inline locale map resolved against the display locale |
796-
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string }` | optional | What the grid draws instead of an empty table: `{ title, message, icon }` — the list view's own empty-state shape |
796+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | What the grid draws instead of an empty table: `{ title, message, icon }` — the list view's own empty-state shape |
797797
| **columns** | `string[] \| { field: string; label?: string \| Record<string, string>; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Columns — all field-name strings, or all column entries `{ field, label?, width?, align?, hidden?, sortable?, … }`, the same union a list view's `columns` declares. One spelling per list: an array mixing strings and column objects is refused |
798798
| **fields** | `string[]` | optional | Field-name fallback the grid reads when `columns` is absent — bare field names (`['name', 'amount']`); write column decoration such as `label` or `width` on `columns` |
799799
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. THE key, singular — not the plural misspelling. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` |
@@ -837,6 +837,7 @@ Sort field and direction pair
837837
| **title** | `string \| Record<string, string>` | optional | Heading of the empty state |
838838
| **message** | `string \| Record<string, string>` | optional | Line of text below the heading |
839839
| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph |
840+
| **link** | `{ label: string \| Record<string, string>; url: string }` | optional | [PLANNED — not yet rendered] The one next step an empty list names: a link the user follows, `{ label, url }` — for example to the how-to page that explains what belongs here when nobody can add a row. It opens a page or an address and runs no action. The console's empty-state renderers are its planned readers; until they ship, the link parses and is not drawn. |
840841

841842
### Nested Shape: `ObjectGridProps.columns[number]`
842843

‎content/docs/references/ui/page.mdx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Interface-level page configuration (Airtable parity)
9999
| **buttons** | `string[]` | optional | Toolbar buttons — names of the source object's actions to surface in the page toolbar |
100100
| **recordAction** | `Enum<'drawer' \| 'page' \| 'modal' \| 'none'>` | optional | How clicking a record opens its detail (drawer \| page \| modal \| none). Default: drawer |
101101
| **showRecordCount** | `boolean` | optional | Show record count at page bottom |
102+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | [PLANNED — not yet rendered] What the page draws in place of rows when its list has none to show: `{ title, message, icon, link }`, the list view's own empty-state shape. The console's list page is its planned reader; until it ships, the page draws a source view's empty state, if any. |
102103
| **allowPrinting** | `boolean` | optional | Allow users to print the page |
103104

104105
### Nested Shape: `InterfacePageConfig.columns[number]`
@@ -181,6 +182,15 @@ View filter rule
181182
| **mode** | `Enum<'inline' \| 'form' \| 'modal'>` | optional (default: `"inline"`) | How to add a new record |
182183
| **formView** | `string` | optional | Named form view to use when mode is "form" or "modal" |
183184

185+
### Nested Shape: `InterfacePageConfig.emptyState`
186+
187+
| Property | Type | Required | Description |
188+
| :--- | :--- | :--- | :--- |
189+
| **title** | `string \| Record<string, string>` | optional | Heading of the empty state |
190+
| **message** | `string \| Record<string, string>` | optional | Line of text below the heading |
191+
| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph |
192+
| **link** | `{ label: string \| Record<string, string>; url: string }` | optional | [PLANNED — not yet rendered] The one next step an empty list names: a link the user follows, `{ label, url }` — for example to the how-to page that explains what belongs here when nobody can add a row. It opens a page or an address and runs no action. The console's empty-state renderers are its planned readers; until they ship, the link parses and is not drawn. |
193+
184194

185195
---
186196

@@ -250,6 +260,7 @@ View filter rule
250260
| **buttons** | `string[]` | optional | Toolbar buttons — names of the source object's actions to surface in the page toolbar |
251261
| **recordAction** | `Enum<'drawer' \| 'page' \| 'modal' \| 'none'>` | optional | How clicking a record opens its detail (drawer \| page \| modal \| none). Default: drawer |
252262
| **showRecordCount** | `boolean` | optional | Show record count at page bottom |
263+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | [PLANNED — not yet rendered] What the page draws in place of rows when its list has none to show: `{ title, message, icon, link }`, the list view's own empty-state shape. The console's list page is its planned reader; until it ships, the page draws a source view's empty state, if any. |
253264
| **allowPrinting** | `boolean` | optional | Allow users to print the page |
254265

255266
### Nested Shape: `Page.aria`

0 commit comments

Comments
 (0)