From 91d0716e272b9b09554ebf3df9878bf9c93b9bd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 13:50:00 +0000 Subject: [PATCH 1/6] feat(spec): object-grid declares description, emptyState and keyboardNavigation The list view's inline empty-state shape is extracted into the exported EmptyStateSchema and taken by reference on both doors; its action/button guidance is reworded to read true on the object-grid door as well. Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude --- packages/spec/src/ui/component.zod.ts | 62 +++++++++++++++++++++++ packages/spec/src/ui/view.zod.ts | 73 ++++++++++++++++++++------- 2 files changed, 117 insertions(+), 18 deletions(-) diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 39065a62d01..c06c3e865b0 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -22,6 +22,10 @@ import { // read `grouping.fields[i].field` (the kanban only `fields[0]`, as its // swimlane fallback), so one declaration judges every door that carries it. GroupingConfigSchema, + // [#20694] `object-grid.emptyState` is the list view's own empty state, taken + // by reference: the grid draws the same key through the same shared + // component `ListView` does, so one declaration judges both doors. + EmptyStateSchema, } from './view.zod'; import { InlineActionSchema, ActionLocationSchema } from './action.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; @@ -3398,6 +3402,18 @@ const GridPageSizeSchema = z.number().int().positive(); * (:2599), `frozenColumns` (:2135), `showColumnTypeIcons` (:1296 …), * `exportOptions`/`operations` (:1697-1721), `label`/`title` (:1732, :2557), * `data`/`staticData` (:372, :386). + * + * [#20694] Three keys measured later, at the `.objectui-sha` pin + * `db11afd4967c` (objectui#11130's merge commit), in the same file: + * `description` (:5677, `resolveInlineI18nLabel(schema.description, + * displayLocale)`, drawn above the rows at :5784 / :6303 / :6342) and + * `emptyState` (:6225, drawn through `DataEmptyState` at :6233). The third, + * `keyboardNavigation`, has NO read point at that pin — zero hits under + * objectui `packages/` and `apps/` outside tests, CHANGELOGs, READMEs and + * `packages/types` (its type declaration and zod twin), against 3 hits for the + * control `schema.editable` in `ObjectGrid.tsx`. It is declared ahead of its + * reader on purpose (the BUILD objectui#11068 chose), and its describe carries + * the `[EXPERIMENTAL — not enforced]` marker that says so; see the member. */ export const ObjectGridPropsSchema = lazySchema(() => strictObject({ surface: 'this `object-grid`', @@ -3409,6 +3425,38 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({ .describe('Object this grid binds to. Optional because the component-level `dataSource` binding can supply the object instead'), label: I18nLabelSchema.optional().describe('Grid label — used as the table caption and export file title'), title: I18nLabelSchema.optional().describe('Fallback for `label` (the renderer reads `label || title`)'), + /** + * [#20694] One line of help text above the grid's rows, in the treatment + * `ListView` gives a view's description. Read at the pin `db11afd4967c` + * (`ObjectGrid.tsx:5677`) through `resolveInlineI18nLabel` against the + * display locale, exactly as `label` is, so both `I18nLabel` forms draw; a + * locale map with no usable entry draws no strip. Drawn by every branch that + * draws rows — the card view, the split pane and the table. + */ + description: I18nLabelSchema.optional() + .describe('One line of help text drawn above the grid\'s rows — a string, or an inline locale map resolved against the display locale'), + /** + * [#20694] What the grid draws INSTEAD of an empty table — the list view's + * own {@link EmptyStateSchema}, by reference (triage's direction: ⛔ not a + * second shape). Read at the pin `db11afd4967c` (`ObjectGrid.tsx:6225`), + * drawn through the shared `DataEmptyState` and `resolveIcon` that `ListView` + * draws a list's empty state with, and only when the grid holds no row to + * draw (no group, when grouped), nothing is loading, and the grid's own + * search box is not what emptied it. A member left out keeps the grid's + * default: the shared glyph, the table's "No results found" heading, no + * message line. + * + * ⚠️ Measured at that pin: `title` and `message` reach `DataEmptyState` as + * they are (`:6241-6242`), with no locale-map resolution, unlike + * `description` above. So of the two `I18nLabel` forms this shared shape + * declares, the grid draws the plain string only — an inline locale map is + * handed to React as a child, which throws (measured on react 19.2.8: + * "Objects are not valid as a React child"). That is a renderer gap on the + * objectui side, not a second shape here: the list view door declares the + * same members, and objectui's grid follows this declaration. + */ + emptyState: EmptyStateSchema.optional() + .describe('What the grid draws instead of an empty table: `{ title, message, icon }` — the list view\'s own empty-state shape'), columns: z.array(z.unknown()).optional() .describe('Columns: field names or column definition objects'), fields: z.array(z.unknown()).optional() @@ -3621,6 +3669,20 @@ export const ObjectGridPropsSchema = lazySchema(() => strictObject({ navigation: z.unknown().optional().describe('Row-click navigation config ({ mode: page | drawer | modal | split | popover | new_window | none }) — all seven `NavigationModeSchema` values, since the shared `useNavigationOverlay` hook types its own mode union as that schema'), editable: z.boolean().optional().describe('Enable inline cell editing'), singleClickEdit: z.boolean().optional().describe('Enter cell edit on single click (default true when editable)'), + /** + * [#20694] Declared AHEAD of its reader, deliberately: objectui#11068 chose + * to BUILD arrow-key cell navigation for the grid, and this row is the spec + * half triage folded in. At the pin `db11afd4967c` nothing reads it (see the + * block docblock above for the measurement), so an authored value changes + * nothing yet. The `[EXPERIMENTAL — not enforced]` marker in the describe is + * the liveness ledger's own spelling for a declared-but-not-enforced key; + * the `page/regions` container that holds page components is undrilled in + * the ledger (`undrilled-containers.baseline.json`), so no ledger row exists + * for any `ComponentPropsMap` key and the marker is the record. When the + * BUILD lands and the grid reads it, drop the marker in the same change. + */ + keyboardNavigation: z.boolean().optional() + .describe('[EXPERIMENTAL — not enforced] Arrow-key cell navigation on the WAI-ARIA grid pattern. Defaults to on when `editable` is set; a read-only grid keeps its Tab behaviour unless this is `true`. No renderer reads it yet: it is declared ahead of the grid\'s keyboard-navigation build, so authoring it changes nothing today'), resizable: z.boolean().optional().describe('Allow column resize (read before `resizableColumns`)'), resizableColumns: z.boolean().optional().describe('Alternate spelling of `resizable` (the renderer reads `resizable ?? resizableColumns`)'), reorderableColumns: z.boolean().optional().describe('Allow column drag-reorder'), diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 6b06e119b34..5f4d9aea0ab 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -1719,6 +1719,56 @@ export const AddRecordConfigSchema = lazySchema(() => strictObject({ formView: z.string().optional().describe('Named form view to use when mode is "form" or "modal"'), }).describe('Add record entry point configuration')); +/** + * The one answer an empty state gives to an author reaching for a call to + * action inside it. Written for EVERY door that carries + * {@link EmptyStateSchema}, the `VIEW_ROW_BOUND_GUIDANCE` way: the add-record + * entry point is a list view's own `addRecord` block, and the `object-grid` + * page block declares no authorable one at all, so a sentence pointing at + * "this list view" would be a wrong answer on the grid. + */ +const EMPTY_STATE_ACTION_GUIDANCE = + 'The empty state renders text only — a `title`, a `message` and an `icon`. An "add record" ' + + 'entry point is not part of it: on a list view, configure it in that view\'s own `addRecord` block.'; + +/** + * Empty State — what a record list draws in place of rows when it has none to + * show: a heading, a line of text and an icon. + * + * [#20694] ONE declaration for every door that carries it, taken BY REFERENCE — + * the list view's `emptyState` below and the `object-grid` page block's + * (`ComponentPropsMap['object-grid'].emptyState`, `component.zod.ts`), the + * `GroupingConfigSchema` precedent (#20831). It was an inline shape on the list + * view until the grid's renderer started reading the same key; a second copy of + * the shape for the grid would be a second thing to drift, and objectui's own + * mirror of the grid's key follows this one, not the other way round. + * + * The extraction left the list view's accept set unchanged: a 28-value parse + * corpus over its four doors (`ListViewSchema`, `ObjectListViewSchema`, + * `ViewSchema.list`, the flattened overlay arm of `ViewMetadataSchema`) reads + * the same success, parsed data, issue codes and paths before and after. The + * one deliberate text change is {@link EMPTY_STATE_ACTION_GUIDANCE}, which now + * reads true on both doors. + */ +export const EmptyStateSchema = lazySchema(() => strictObject({ + surface: 'this empty state', + history: VIEW_HISTORY, + // `description`/`text`/`subtitle` are the words the neighbouring empty-state + // vocabularies use for the secondary line; here it is `message`. + aliases: { description: 'message', text: 'message', subtitle: 'message', heading: 'title', label: 'title', image: 'icon' }, + // An author wiring a CTA into the empty state is reaching for the add-record + // entry point, which is a separate block where one exists at all. + guidance: { + action: EMPTY_STATE_ACTION_GUIDANCE, + button: EMPTY_STATE_ACTION_GUIDANCE, + }, +}, { + title: I18nLabelSchema.optional().describe('Heading of the empty state'), + message: I18nLabelSchema.optional().describe('Line of text below the heading'), + icon: z.string().optional() + .describe('Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph'), +})); + /** * Kanban Settings */ @@ -2872,24 +2922,11 @@ const ListViewShapeSchema = lazySchema(() => strictObject({ /** Advanced: Allow Printing (Airtable Interface parity) */ allowPrinting: z.boolean().optional().describe('Allow users to print the view'), - /** Empty State */ - emptyState: strictObject({ - surface: 'this empty state', - history: VIEW_HISTORY, - // `description`/`text`/`subtitle` are the words the neighbouring empty-state - // vocabularies use for the secondary line; here it is `message`. - aliases: { description: 'message', text: 'message', subtitle: 'message', heading: 'title', label: 'title', image: 'icon' }, - // The add-record entry point is a real, separate block on this same view — - // an author wiring a CTA into the empty state is reaching for it. - guidance: { - action: 'The empty state renders text only. Configure the "add record" entry point in the `addRecord` block on this list view.', - button: 'The empty state renders text only. Configure the "add record" entry point in the `addRecord` block on this list view.', - }, - }, { - title: I18nLabelSchema.optional(), - message: I18nLabelSchema.optional(), - icon: z.string().optional(), - }).optional().describe('Empty state configuration when no records found'), + /** + * Empty State — {@link EmptyStateSchema} by reference (#20694), the same + * declaration the `object-grid` page block carries. + */ + emptyState: EmptyStateSchema.optional().describe('Empty state configuration when no records found'), /** ARIA accessibility attributes */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the list view'), From 8fe9d7972a2b7f420a24eb15151e23da007265ba Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 13:51:37 +0000 Subject: [PATCH 2/6] test(spec): pin object-grid's description, emptyState and keyboardNavigation The three members parse byte-identical, the row still refuses an unknown key and a wrong value in each member (code and path), and both the grid's and the list view's emptyState hold EmptyStateSchema itself. Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude --- ...nent-object-grid-declared-keys.pin.test.ts | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 packages/spec/src/ui/component-object-grid-declared-keys.pin.test.ts diff --git a/packages/spec/src/ui/component-object-grid-declared-keys.pin.test.ts b/packages/spec/src/ui/component-object-grid-declared-keys.pin.test.ts new file mode 100644 index 00000000000..693b89f5aac --- /dev/null +++ b/packages/spec/src/ui/component-object-grid-declared-keys.pin.test.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#20694] `object-grid` declares `description`, `emptyState` and + * `keyboardNavigation` — the keys objectui's `ObjectGrid` reads (the first + * two) or is being built to read (the third), which the strict row refused. + * + * ## The gap this file closes + * + * `ComponentPropsMap['object-grid']` is a strict shape derived from the + * renderer's own read points, and it lagged the renderer: at the + * `.objectui-sha` pin the grid reads `schema.description` and + * `schema.emptyState`, while this row refused both by name. A metadata author + * validating against the row had a key the renderer honours reported as + * unknown, and objectui's registry could not list it either (its parity test + * refuses an input the spec does not accept). + * + * ## What is pinned, and why each half + * + * - §1 THE THREE MEMBERS PARSE, byte-identical: both `I18nLabel` forms of + * `description`, a full `emptyState`, and both booleans of + * `keyboardNavigation`. An absent member stays absent. + * - §2 THE ROW IS STILL CLOSED: an unknown top-level key is refused, and so is + * a wrong value in each new member. Each is the code AND the path, so a + * refusal for the wrong reason reds; the unknown-key control is what proves + * the widening did not open the row. + * - §3 ONE SCHEMA: the grid's `emptyState` and the list view's are + * `EmptyStateSchema` itself, and the grid answers exactly what it answers on + * every value in the corpus — triage ruled out a second shape, and a later + * copy reds the identity half even while it still agrees on today's corpus. + */ + +import { describe, it, expect } from 'vitest'; +import type { z } from 'zod'; + +import { ComponentPropsMap, ObjectGridPropsSchema } from './component.zod'; +import { EmptyStateSchema, ListViewSchema } from './view.zod'; + +const GRID = () => ComponentPropsMap['object-grid']; +const BASE = { objectName: 'account' } as const; + +/** The issue codes and paths a refusal carries, so a refusal for the WRONG reason reds. */ +function issues(result: z.ZodSafeParseResult): { code: string; path: string }[] { + if (result.success) return []; + return result.error.issues.map((i) => ({ code: i.code, path: i.path.join('.') })); +} + +// ─────────────────────────────────────────────────────────────────────────── +// §1 the three members parse +// ─────────────────────────────────────────────────────────────────────────── + +describe('§1 object-grid accepts description, emptyState and keyboardNavigation', () => { + const ACCEPTED: ReadonlyArray]> = [ + ['description as a string', { description: 'Accounts you own' }], + ['description as an inline locale map', { description: { en: 'Accounts you own', 'zh-CN': '你负责的客户' } }], + ['a full emptyState', { emptyState: { title: 'No contacts yet', message: 'Add one to get started', icon: 'users' } }], + ['an emptyState with one member', { emptyState: { message: 'Nothing here yet' } }], + ['keyboardNavigation: true', { keyboardNavigation: true }], + ['keyboardNavigation: false', { keyboardNavigation: false }], + ['all three together', { + description: 'Accounts you own', + emptyState: { title: 'No accounts' }, + keyboardNavigation: true, + editable: true, + }], + ]; + + for (const [label, props] of ACCEPTED) { + it(`parses ${label}, byte-identical`, () => { + const r = GRID().safeParse({ ...BASE, ...props }); + expect(issues(r)).toEqual([]); + expect(r.success && r.data).toStrictEqual({ ...BASE, ...props }); + }); + } + + it('an absent member stays absent', () => { + const r = GRID().safeParse(BASE); + expect(issues(r)).toEqual([]); + expect(r.success && Object.keys(r.data)).toEqual(['objectName']); + }); +}); + +// ─────────────────────────────────────────────────────────────────────────── +// §2 the row is still closed +// ─────────────────────────────────────────────────────────────────────────── + +describe('§2 object-grid still refuses what it does not declare', () => { + it('LIT CONTROL — an unknown top-level key is refused: unrecognized_keys at the row itself', () => { + const r = GRID().safeParse({ ...BASE, description: 'x', notAGridKey: 1 }); + expect(r.success).toBe(false); + expect(issues(r)).toEqual([{ code: 'unrecognized_keys', path: '' }]); + expect(r.success ? [] : (r.error.issues[0] as { keys?: string[] }).keys).toEqual(['notAGridKey']); + }); + + const REFUSED: ReadonlyArray, code: string, path: string]> = [ + ['a numeric description', { description: 7 }, 'invalid_union', 'description'], + ['a string keyboardNavigation', { keyboardNavigation: 'yes' }, 'invalid_type', 'keyboardNavigation'], + ['a bare-string emptyState', { emptyState: 'Nothing here' }, 'invalid_type', 'emptyState'], + ['a numeric emptyState title', { emptyState: { title: 42 } }, 'invalid_union', 'emptyState.title'], + ['an unknown emptyState member', { emptyState: { title: 'None', notAnEmptyStateKey: 1 } }, 'unrecognized_keys', 'emptyState'], + ['an emptyState call to action', { emptyState: { action: { label: 'Add' } } }, 'unrecognized_keys', 'emptyState'], + ]; + + for (const [label, props, code, path] of REFUSED) { + it(`refuses ${label} — ${code} at ${path}`, () => { + const r = GRID().safeParse({ ...BASE, ...props }); + expect(r.success).toBe(false); + expect(issues(r)).toEqual([{ code, path }]); + }); + } +}); + +// ─────────────────────────────────────────────────────────────────────────── +// §3 one schema, not a copy of its shape +// ─────────────────────────────────────────────────────────────────────────── + +describe('§3 the grid and the list view hold EmptyStateSchema itself', () => { + const HELD = [ + ['object-grid', () => ObjectGridPropsSchema.shape.emptyState], + ['list-view', () => ListViewSchema.shape.emptyState], + ] as const; + + for (const [door, member] of HELD) { + it(`${door}: the emptyState member unwraps to EmptyStateSchema — the same def, by identity`, () => { + expect(member().unwrap()._zod.def).toBe(EmptyStateSchema._zod.def); + }); + } + + it('object-grid answers what EmptyStateSchema answers, one level down, on every corpus value', () => { + const corpus: unknown[] = [ + {}, + { title: 'No contacts yet', message: 'Add one to get started', icon: 'users' }, + { title: { en: 'None', 'zh-CN': '无' } }, + { title: 42 }, + { icon: 5 }, + { description: 'x' }, + { action: {} }, + { notAnEmptyStateKey: 1 }, + 'Nothing here', + null, + ]; + for (const emptyState of corpus) { + const direct = issues(EmptyStateSchema.safeParse(emptyState)).map(({ code, path }) => ({ + code, + path: path ? `emptyState.${path}` : 'emptyState', + })); + expect(issues(GRID().safeParse({ ...BASE, emptyState }))).toEqual(direct); + } + }); +}); From fc71f3b363c8c208da8d05eaba9d3d27dce70a5c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 13:59:12 +0000 Subject: [PATCH 3/6] feat(spec): export the EmptyState author type beside EmptyStateSchema The reference docs gate refuses a documented schema without its type alias. The schema has no default or transform, so it is pinned isomorphic (ADR-0122) rather than given an EmptyStateParsed synonym. Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude --- packages/spec/src/type-alias-convention.pin.test.ts | 3 +++ packages/spec/src/ui/view.zod.ts | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 068c6a76fe9..a12b0749ebf 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -1583,6 +1583,9 @@ export type Iso_ui_responsive__StyleMapSchema = Assert, z.infer< typeof M167.CalendarConfigSchema > >>; export type Iso_ui_view__ColumnSummaryConfigSchema = Assert, z.infer< typeof M167.ColumnSummaryConfigSchema > >>; export type Iso_ui_view__ColumnSummarySchema = Assert, z.infer< typeof M167.ColumnSummarySchema > >>; +// `EmptyStateSchema` joined on #20694, extracted from the list view's inline +// `emptyState` so the `object-grid` row can take it by reference. +export type Iso_ui_view__EmptyStateSchema = Assert, z.infer< typeof M167.EmptyStateSchema > >>; export type Iso_ui_view__FormButtonConfigSchema = Assert, z.infer< typeof M167.FormButtonConfigSchema > >>; export type Iso_ui_view__GanttConfigSchema = Assert, z.infer< typeof M167.GanttConfigSchema > >>; export type Iso_ui_view__GanttQuickFilterSchema = Assert, z.infer< typeof M167.GanttQuickFilterSchema > >>; diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 5f4d9aea0ab..326e374db2d 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -7183,6 +7183,12 @@ export type RowHeight = z.input; export type GroupingConfig = z.input; /** Post-parse shape of {@link GroupingConfig} — defaults applied, transforms run (ADR-0122). */ export type GroupingConfigParsed = z.infer; +/** + * Authoring shape of {@link EmptyStateSchema} — no default or transform in its + * tree, so it has no `EmptyStateParsed` (ADR-0122; pinned isomorphic in + * `type-alias-convention.pin.test.ts`). + */ +export type EmptyState = z.input; export type GalleryConfig = z.input; /** Post-parse shape of {@link GalleryConfig} — defaults applied, transforms run (ADR-0122). */ export type GalleryConfigParsed = z.infer; From 34954b08f18bbffedabe5d0301f29425506080e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 14:05:53 +0000 Subject: [PATCH 4/6] chore(spec): regenerate artifacts and add the minor changeset Regenerated by the build and `check:generated --fix`: authorable-surface, json-schema manifest, api-surface, export-origins, declaration-map and the reference docs. Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude --- ...ription-empty-state-keyboard-navigation.md | 17 +++++++++++ content/docs/references/index.mdx | 10 +++---- content/docs/references/ui/component.mdx | 11 +++++++ content/docs/references/ui/view.mdx | 29 ++++++++++++++----- packages/spec/api-surface/ui.json | 2 ++ packages/spec/authorable-surface/ui.json | 6 ++++ packages/spec/declaration-map/ui.json | 2 ++ packages/spec/export-origins/ui.json | 2 ++ packages/spec/json-schema.manifest/ui.json | 1 + 9 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 .changeset/20694-object-grid-description-empty-state-keyboard-navigation.md diff --git a/.changeset/20694-object-grid-description-empty-state-keyboard-navigation.md b/.changeset/20694-object-grid-description-empty-state-keyboard-navigation.md new file mode 100644 index 00000000000..7ab8dd707c6 --- /dev/null +++ b/.changeset/20694-object-grid-description-empty-state-keyboard-navigation.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': minor +--- + +The `object-grid` page block now declares `description`, `emptyState` and `keyboardNavigation`, so a page that authors them validates clean instead of having each reported as a prop the block does not declare (#20694). + +Clause-②: yes (widening) + +- **`description`** — an `I18nLabel` (a string, or an inline locale map): one line of help text the grid draws above its rows, resolved against the display locale. +- **`emptyState`** — `{ title?, message?, icon? }`, what the grid draws instead of an empty table. It is the list view's own empty-state shape, now exported as `EmptyStateSchema` (author type `EmptyState`) and taken by reference on both `list-view` and `object-grid`, so the two cannot drift apart. +- **`keyboardNavigation`** — a boolean, marked `[EXPERIMENTAL — not enforced]`: arrow-key cell navigation on the WAI-ARIA grid pattern, on by default when `editable` is set. No renderer reads it yet, so authoring it changes nothing today. + +Nothing that parsed before is refused now. The `object-grid` row still refuses every key it does not declare, and the list view's `emptyState` accepts exactly the values it accepted before the shape was extracted. One refusal message is reworded: an `action` or `button` key inside an empty state is still refused and still points at the list view's `addRecord` block, now phrased so that it also reads true on `object-grid`, which has no add-record block. + +On `object-grid`, write `emptyState.title` and `emptyState.message` as plain strings for now: the grid renderer draws both as they are and does not yet resolve an inline locale map there the way it resolves `description`, so a map in either member fails to render. + +Where it surfaces: the component-props gate (`os validate`, `os build`, `os lint`) no longer reports these three keys on an `object-grid` node, and the published JSON Schema for `ObjectGridProps` describes them. diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index eb443f09f76..b210b054b1b 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1521 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1522 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 165 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **196** | **1521** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **196** | **1522** | 14 protocol modules | --- @@ -363,7 +363,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 165 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 166 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -384,7 +384,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | | [`responsive.zod.ts`](/docs/references/ui/responsive) | `ResponsiveStyles`, `StyleMap` | | [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` | -| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormSelectOption`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | +| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `EmptyState`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormSelectOption`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | --- diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 7df5f29b50e..bd4571b42ff 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -693,6 +693,8 @@ Sort field and direction pair | **objectName** | `string` | optional | Object this grid binds to. Optional because the component-level `dataSource` binding can supply the object instead | | **label** | `string \| Record` | optional | Grid label — used as the table caption and export file title | | **title** | `string \| Record` | optional | Fallback for `label` (the renderer reads `label \|\| title`) | +| **description** | `string \| Record` | optional | One line of help text drawn above the grid's rows — a string, or an inline locale map resolved against the display locale | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | What the grid draws instead of an empty table: `{ title, message, icon }` — the list view's own empty-state shape | | **columns** | `any[]` | optional | Columns: field names or column definition objects | | **fields** | `any[]` | optional | Field list fallback used when `columns` is absent | | **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` | @@ -718,6 +720,7 @@ Sort field and direction pair | **navigation** | `any` | optional | Row-click navigation config (`{ mode: page \| drawer \| modal \| split \| popover \| new_window \| none }`) — all seven `NavigationModeSchema` values, since the shared `useNavigationOverlay` hook types its own mode union as that schema | | **editable** | `boolean` | optional | Enable inline cell editing | | **singleClickEdit** | `boolean` | optional | Enter cell edit on single click (default true when editable) | +| **keyboardNavigation** | `boolean` | optional | [EXPERIMENTAL — not enforced] Arrow-key cell navigation on the WAI-ARIA grid pattern. Defaults to on when `editable` is set; a read-only grid keeps its Tab behaviour unless this is `true`. No renderer reads it yet: it is declared ahead of the grid's keyboard-navigation build, so authoring it changes nothing today | | **resizable** | `boolean` | optional | Allow column resize (read before `resizableColumns`) | | **resizableColumns** | `boolean` | optional | Alternate spelling of `resizable` (the renderer reads `resizable ?? resizableColumns`) | | **reorderableColumns** | `boolean` | optional | Allow column drag-reorder | @@ -728,6 +731,14 @@ Sort field and direction pair | **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source binding (ViewDataSchema — discriminated on `provider`: object \| api \| value \| schema). Static inline rows live at `{ provider: 'value', items: [...] }`; the bare-array shortcut is refused — see migration `object-grid-data-view-data-converged` | | **staticData** | `any[]` | optional | Deprecated bare-array static-rows shortcut the renderer still reads. Prefer `data: { provider: 'value', items: [...] }` | +### Nested Shape: `ObjectGridProps.emptyState` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string \| Record` | optional | Heading of the empty state | +| **message** | `string \| Record` | optional | Line of text below the heading | +| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph | + ### Nested Shape: `ObjectGridProps.filter[number]` View filter rule diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 18a962ee89c..92428e28528 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -59,8 +59,8 @@ nothing. The one item-name grammar itself lives in ## TypeScript Usage ```typescript -import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormFieldPublicPickerSchema, FormSectionSchema, FormSelectOptionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListMapConfigSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui'; -import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormFieldPublicPicker, FormSection, FormSelectOption, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, HttpMethodSubset, HttpRequest, KanbanConfig, ListChartConfig, ListColumn, ListMapConfig, ListView, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui'; +import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, EmptyStateSchema, FormButtonConfigSchema, FormFieldSchema, FormFieldPublicPickerSchema, FormSectionSchema, FormSelectOptionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListMapConfigSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui'; +import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, EmptyState, FormButtonConfig, FormField, FormFieldPublicPicker, FormSection, FormSelectOption, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, HttpMethodSubset, HttpRequest, KanbanConfig, ListChartConfig, ListColumn, ListMapConfig, ListView, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui'; // Validate data const result = AddRecordConfigSchema.parse(data); @@ -160,6 +160,19 @@ Column footer summary configuration | **field** | `string` | optional | Field to aggregate (defaults to the column field) | +--- + +## EmptyState + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **title** | `string \| Record` | optional | Heading of the empty state | +| **message** | `string \| Record` | optional | Line of text below the heading | +| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph | + + --- ## FormButtonConfig @@ -1128,9 +1141,9 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **title** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | -| **message** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | -| **icon** | `string` | optional | | +| **title** | `string \| Record` | optional | Heading of the empty state | +| **message** | `string \| Record` | optional | Line of text below the heading | +| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph | ### Nested Shape: `ListView.aria` @@ -1511,9 +1524,9 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **title** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | -| **message** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | -| **icon** | `string` | optional | | +| **title** | `string \| Record` | optional | Heading of the empty state | +| **message** | `string \| Record` | optional | Line of text below the heading | +| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph | ### Nested Shape: `ObjectListView.aria` diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index fc7584b8749..f9afa5ab724 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -175,6 +175,8 @@ "ElementRepeaterPropsSchema (const)", "ElementTextInputPropsSchema (const)", "ElementTextPropsSchema (const)", + "EmptyState (type)", + "EmptyStateSchema (const)", "ExpandViewResult (interface)", "ExpandedViewItem (interface)", "ExpressionBindableTextKey (type)", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index f034e256898..2ba73d88da3 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -515,6 +515,9 @@ "ui/ElementTextProps:aria", "ui/ElementTextProps:content", "ui/ElementTextProps:variant", + "ui/EmptyState:icon", + "ui/EmptyState:message", + "ui/EmptyState:title", "ui/FormButtonConfig:label", "ui/FormButtonConfig:show", "ui/FormField:colSpan", @@ -870,12 +873,15 @@ "ui/ObjectGridProps:data", "ui/ObjectGridProps:defaultFilters", "ui/ObjectGridProps:defaultSort [RETIRED]", + "ui/ObjectGridProps:description", "ui/ObjectGridProps:editable", + "ui/ObjectGridProps:emptyState", "ui/ObjectGridProps:exportOptions", "ui/ObjectGridProps:fields", "ui/ObjectGridProps:filter", "ui/ObjectGridProps:frozenColumns", "ui/ObjectGridProps:grouping", + "ui/ObjectGridProps:keyboardNavigation", "ui/ObjectGridProps:label", "ui/ObjectGridProps:navigation", "ui/ObjectGridProps:objectName", diff --git a/packages/spec/declaration-map/ui.json b/packages/spec/declaration-map/ui.json index 31ffbe0df1b..7477d4f958c 100644 --- a/packages/spec/declaration-map/ui.json +++ b/packages/spec/declaration-map/ui.json @@ -114,6 +114,8 @@ "ElementRepeaterPropsSchema": "ui/ElementRepeaterProps", "ElementTextInputPropsSchema": "ui/ElementTextInputProps", "ElementTextPropsSchema": "ui/ElementTextProps", + "EmptyState": "ui/EmptyState", + "EmptyStateSchema": "ui/EmptyState", "ExpressionBindableTextKey": "ui/ExpressionBindableTextKey", "ExpressionBindableTextKeySchema": "ui/ExpressionBindableTextKey", "FormButtonConfig": "ui/FormButtonConfig", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index cd0bd4be19f..39d74176be4 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -171,6 +171,8 @@ "ElementRepeaterPropsSchema": "src/ui/component.zod.ts#ElementRepeaterPropsSchema (const)", "ElementTextInputPropsSchema": "src/ui/component.zod.ts#ElementTextInputPropsSchema (const)", "ElementTextPropsSchema": "src/ui/component.zod.ts#ElementTextPropsSchema (const)", + "EmptyState": "src/ui/view.zod.ts#EmptyState (type)", + "EmptyStateSchema": "src/ui/view.zod.ts#EmptyStateSchema (const)", "ExpandViewResult": "src/ui/view.zod.ts#ExpandViewResult (interface)", "ExpandedViewItem": "src/ui/view.zod.ts#ExpandedViewItem (interface)", "ExpressionBindableTextKey": "src/ui/expression-bindable-text-keys.zod.ts#ExpressionBindableTextKey (type)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index d3c58865bf9..cf577ded7ec 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -63,6 +63,7 @@ "ui/ElementRepeaterProps", "ui/ElementTextInputProps", "ui/ElementTextProps", + "ui/EmptyState", "ui/ExpressionBindableTextKey", "ui/FormButtonConfig", "ui/FormField", From 0e7831e13211d6b1f06c56d41d5f8d77b1635c07 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 14:15:06 +0000 Subject: [PATCH 5/6] test(spec): count the EmptyState isomorphic pin (779 -> 780) Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude --- packages/spec/src/type-alias-convention.pin.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index a12b0749ebf..b40846b6c23 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -275,7 +275,7 @@ import type * as M187 from './shared/duration.zod.js'; import type * as M188 from './ai/build-progress.zod.js'; // --------------------------------------------------------------------------- -// 779 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 780 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -1669,7 +1669,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 779 isomorphic pins', () => { + it('still declares all 780 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2376,7 +2376,14 @@ describe('ADR-0122 type-alias convention', () => { // `interval` rename's `retiredKey()`, kept input and output equal — so the // pin was the whole of its footprint here. The M78 slot stays occupied by // the module's surviving pins. -1 removed. - expect(pins).toHaveLength(779); + // + // 779 -> 780 is #20694: the list view's inline `emptyState` shape was + // extracted into the exported `EmptyStateSchema` so the `object-grid` row + // can take it by reference, and the docs gate requires its `EmptyState` + // author alias. It has no default or transform in its tree, so it is + // pinned here (Iso_ui_view__EmptyStateSchema) rather than given an + // `EmptyStateParsed` synonym. +1 added. + expect(pins).toHaveLength(780); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until #6605 nothing read either From 98dad84e210101e764efd40d32446187ffed7c92 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 14:35:04 +0000 Subject: [PATCH 6/6] chore(spec): regenerate the artifacts the origin/main merge deferred authorable-surface/ui.json and the reference docs were merged by the os-regen driver without a text merge; regenerated from the merged tree so both sides' entries are present (PageHeaderProps.breadcrumb [RETIRED] from main, the object-grid and EmptyState rows from this branch). Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude --- content/docs/references/index.mdx | 9 +++++---- content/docs/references/ui/component.mdx | 2 +- packages/spec/authorable-surface/ui.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index b210b054b1b..22f03c81d2c 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1522 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1525 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -23,7 +23,7 @@ counts are sums of the rows they head. Regenerate with | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | | [API Protocol](/docs/references/api) | 32 | 429 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 14 | 75 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | -| [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | +| [Data Protocol](/docs/references/data) | 30 | 178 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 16 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | | [Kernel Protocol](/docs/references/kernel) | 30 | 157 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **196** | **1522** | 14 protocol modules | +| **Total** | **197** | **1525** | 14 protocol modules | --- @@ -131,7 +131,7 @@ Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execu ## Data Protocol -**Source:** `packages/spec/src/data/` · **Import:** `@objectstack/spec/data` · **29 pages, 175 schemas** +**Source:** `packages/spec/src/data/` · **Import:** `@objectstack/spec/data` · **30 pages, 178 schemas** Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. @@ -162,6 +162,7 @@ Objects, fields, queries, filters, datasources and drivers — the ObjectQL laye | [`hook-body.zod.ts`](/docs/references/data/hook-body) | `ExpressionBody`, `HookBody`, `HookBodyCapability`, `ScriptBody` | | [`mapping.zod.ts`](/docs/references/data/mapping) | `ImportFieldMapping`, `Mapping`, `TransformType` | | [`object.zod.ts`](/docs/references/data/object) | `ApiMethod`, `ApiOperation`, `Index`, `Lifecycle`, `LifecycleClass`, `Object`, `ObjectAccessConfig`, `ObjectCapabilities`, `ObjectExtension`, `ObjectExternalBinding`, `ObjectFieldGroup`, `ObjectOwnershipEnum`, `ObjectRequiredPermissions`, `PerOperationRequiredPermissions`, `RowCrudActionOverride`, `TenancyConfig` | +| [`picklist.zod.ts`](/docs/references/data/picklist) | `Picklist`, `PicklistExtension`, `PicklistServedField` | | [`query.zod.ts`](/docs/references/data/query) | `AggregationFunction`, `AggregationNode`, `DateGranularity`, `FieldNode`, `FullTextSearch`, `GroupByNode`, `Query`, `SortNode` | | [`seed.zod.ts`](/docs/references/data/seed) | `Seed`, `SeedMode` | | [`seed-loader.zod.ts`](/docs/references/data/seed-loader) | `ObjectDependencyGraph`, `ObjectDependencyNode`, `ReferenceResolution`, `ReferenceResolutionError`, `SeedIdentity`, `SeedLoadResult`, `SeedLoaderConfig`, `SeedLoaderRequest`, `SeedLoaderResult` | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index bd4571b42ff..c68178e7273 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -1209,7 +1209,7 @@ View filter rule | **title** | `string \| Record` | optional | Page title. Omit to let the renderer derive the heading from the record (the default for record pages) — set explicitly on non-record pages (dashboard, landing) with no record to derive from. | | **subtitle** | `string \| Record` | optional | Page subtitle | | **icon** | `never` | optional | [REMOVED] `page:header` property `icon` was removed in @objectstack/spec 17.0.0 (ADR-0087 D2) — no renderer ever read it: objectui resolves `icon` only per header action (`action.icon`), never off the header's own props bag, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key. The header's own identity is drawn by the record chrome (`recordChrome`, on by default) and each action carries its own `icon`. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **breadcrumb** | `boolean` | optional (default: `true`) | Show breadcrumb | +| **breadcrumb** | `never` | optional | [REMOVED] `page:header` property `breadcrumb` was removed in @objectstack/spec 17 (ADR-0087 D2) — no renderer ever drew a trail for it: objectui drew an empty slot and nothing filled it, and the navigation trail is drawn once, by the app shell's header. Delete the key, whether it was `true` or `false`; the shell's trail is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. | | **actions** | `string[]` | optional | Action IDs to show in header | | **recordChrome** | `boolean` | optional (default: `true`) | Render the record chrome — the title as a record chip with its follow star and copy-id button. Set false on a non-record page (dashboard, landing) to fall back to the bare heading layout. | | **showStar** | `boolean` | optional (default: `true`) | Show the follow (favourite) star beside the record title. Part of the record chrome — no effect when `recordChrome` is false. | diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 2ba73d88da3..f3a0fe77801 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -1101,7 +1101,7 @@ "ui/PageContainerProps:children", "ui/PageHeaderProps:actions", "ui/PageHeaderProps:aria", - "ui/PageHeaderProps:breadcrumb", + "ui/PageHeaderProps:breadcrumb [RETIRED]", "ui/PageHeaderProps:icon [RETIRED]", "ui/PageHeaderProps:maxVisible", "ui/PageHeaderProps:mobileMaxVisible",