From 77684e5e76afcb4c584ced4cddf6f62e94ef5a47 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 02:01:58 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat(spec):=20i18n=20label=20contract=20?= =?UTF-8?q?=E2=80=94=20inline=20locale=20maps,=20`=5Ftabs`=20translations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes objectstack-ai/objectstack#5728 Fixes objectstack-ai/objectstack#5377 `I18nLabelSchema` becomes a union of the plain default-language string and an inline BCP-47 locale map, and `ObjectTranslationDataSchema` gains `_tabs` with a resolver that reads it. See the PR body for the per-issue rationale. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY --- content/docs/references/api/metadata.mdx | 2 +- content/docs/references/api/protocol.mdx | 18 +- content/docs/references/data/object.mdx | 4 +- content/docs/references/index.mdx | 10 +- content/docs/references/kernel/manifest.mdx | 2 +- .../references/kernel/metadata-plugin.mdx | 2 +- .../references/system/settings-manifest.mdx | 18 +- .../docs/references/system/translation.mdx | 1 + content/docs/references/ui/action.mdx | 28 +-- content/docs/references/ui/app.mdx | 52 ++--- content/docs/references/ui/chart.mdx | 20 +- content/docs/references/ui/component.mdx | 78 +++---- content/docs/references/ui/dashboard.mdx | 24 +- content/docs/references/ui/dataset.mdx | 12 +- content/docs/references/ui/i18n.mdx | 31 ++- content/docs/references/ui/page.mdx | 22 +- content/docs/references/ui/report.mdx | 24 +- content/docs/references/ui/view.mdx | 76 +++---- ...07-unknown-key-strictness-ledger.counts.md | 2 +- .../src/system/translations/index.ts | 15 ++ packages/cli/src/utils/i18n-extract.ts | 73 ++++++ packages/cli/test/i18n-tab-coverage.test.ts | 208 ++++++++++++++++++ packages/spec/api-surface/system.json | 2 + packages/spec/api-surface/ui.json | 2 + packages/spec/authorable-surface/system.json | 1 + packages/spec/json-schema.manifest/ui.json | 1 + .../spec/src/system/i18n-resolver.test.ts | 186 ++++++++++++++++ packages/spec/src/system/i18n-resolver.ts | 150 +++++++++++++ packages/spec/src/system/translation.zod.ts | 44 +++- packages/spec/src/ui/component.zod.ts | 41 +++- packages/spec/src/ui/i18n.test.ts | 89 ++++++-- packages/spec/src/ui/i18n.zod.ts | 127 +++++++++-- 32 files changed, 1119 insertions(+), 246 deletions(-) create mode 100644 packages/cli/test/i18n-tab-coverage.test.ts diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 35d6a3e68a..84f00f823d 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -53,7 +53,7 @@ const result = AppDefinitionResponseSchema.parse(data); | **success** | `boolean` | ✅ | Operation success status | | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | -| **data** | `{ name: string; label: string; description?: string; icon?: string; … }` | ✅ | Full App Configuration | +| **data** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }` | ✅ | Full App Configuration | --- diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index e7b774e7b7..230b8ae77a 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -389,7 +389,7 @@ const result = AiAgentCapabilitiesSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **object** | `string` | ✅ | Object name (snake_case) | -| **data** | `{ name?: string; label?: string; object?: string; list?: object; … }` | ✅ | View definition to create | +| **data** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }` | ✅ | View definition to create | --- @@ -402,7 +402,7 @@ const result = AiAgentCapabilitiesSchema.parse(data); | :--- | :--- | :--- | :--- | | **object** | `string` | ✅ | Object name | | **viewId** | `string` | ✅ | Created view identifier | -| **view** | `{ name?: string; label?: string; object?: string; list?: object; … }` | ✅ | Created view definition | +| **view** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }` | ✅ | Created view definition | --- @@ -959,11 +959,11 @@ Get package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. | -| **label** | `string` | optional | Human-readable label shown in metadata lists. | +| **label** | `string \| Record` | optional | Human-readable label shown in metadata lists. | | **object** | `string` | optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. | -| **list** | `{ name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | optional | | +| **list** | `{ name?: string; label?: string \| Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | optional | | | **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | | -| **listViews** | `Record; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | | **formViews** | `Record; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -996,7 +996,7 @@ Get package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **object** | `string` | ✅ | Object name | -| **view** | `{ name?: string; label?: string; object?: string; list?: object; … }` | ✅ | View definition | +| **view** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }` | ✅ | View definition | --- @@ -1177,7 +1177,7 @@ List packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **object** | `string` | ✅ | Object name | -| **views** | `{ name?: string; label?: string; object?: string; list?: object; … }[]` | ✅ | Array of view definitions | +| **views** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }[]` | ✅ | Array of view definitions | --- @@ -1596,7 +1596,7 @@ Uninstall package response | :--- | :--- | :--- | :--- | | **object** | `string` | ✅ | Object name (snake_case) | | **viewId** | `string` | ✅ | View identifier | -| **data** | `{ name?: string; label?: string; object?: string; list?: object; … }` | ✅ | Partial view data to update | +| **data** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }` | ✅ | Partial view data to update | --- @@ -1609,7 +1609,7 @@ Uninstall package response | :--- | :--- | :--- | :--- | | **object** | `string` | ✅ | Object name | | **viewId** | `string` | ✅ | Updated view identifier | -| **view** | `{ name?: string; label?: string; object?: string; list?: object; … }` | ✅ | Updated view definition | +| **view** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }` | ✅ | Updated view definition | --- diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index a343475980..ef7b995515 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -136,13 +136,13 @@ const result = ApiMethod.parse(data); | **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. | | **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. | | **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. | -| **listViews** | `Record; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) | | **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. | | **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]; … }` | optional | Enabled system features modules | | **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). | | **externalSharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | [ADR-0090 D11] OWD for external (portal/partner) principals. Defaults to private; must be <= sharingModel in openness. | | **publicSharing** | `{ enabled?: boolean; allowedAudiences?: Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]; allowedPermissions?: Enum<'view' \| 'comment' \| 'edit'>[]; maxExpiryDays?: integer; … }` | optional | Public share-link policy (Notion/Figma-style link sharing) | -| **actions** | `{ name: string; label: string; objectName?: string; icon?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) | +| **actions** | `{ name: string; label: string \| Record; objectName?: string; icon?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this object. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 19de4e24de..0a883d0d6b 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1601 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1602 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/. */} @@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 8 | 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) | 37 | 295 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 146 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **200** | **1601** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 147 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **200** | **1602** | 14 protocol modules | --- @@ -366,7 +366,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 146 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 147 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -380,7 +380,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ElementButtonProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementTextInputProps`, `ElementTextProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordPathProps`, `RecordRelatedListProps` | | [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` | | [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetMeasure`, `DerivedMeasureOp` | -| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel` | +| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel`, `InlineLocaleMap` | | [`notification.zod.ts`](/docs/references/ui/notification) | `NotificationPosition`, `NotificationSeverity`, `NotificationType` | | [`page.zod.ts`](/docs/references/ui/page) | `ElementDataSource`, `InterfacePageConfig`, `Page`, `PageComponent`, `PageComponentType`, `PageRegion`, `PageType`, `PageVariable` | | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index b7a54c7a72..6baf1e215b 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -44,7 +44,7 @@ const result = ManifestSchema.parse(data); | **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | | **capabilities** | `{ implements?: { protocol: object; conformance?: Enum<'full' \| 'partial' \| 'experimental' \| 'deprecated'>; implementedFeatures?: string[]; features?: { name: string; enabled?: boolean; description?: string; sinceVersion?: string; … }[]; … }[]; provides?: { id: string; name: string; description?: string; version: object; … }[]; requires?: { pluginId: string; version: string; optional?: boolean; reason?: string; … }[]; extensionPoints?: { id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]; … }` | optional | Plugin capability declarations for interoperability | | **extensions** | `Record` | optional | Extension points and contributions | -| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: ({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: ({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| … +5 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | | **loading** | `{ strategy?: Enum<'eager' \| 'lazy' \| 'parallel' \| 'deferred' \| 'on-demand'>; preload?: object; codeSplitting?: object; dynamicImport?: object; … }` | optional | Plugin loading and runtime behavior configuration | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 43c7ed896f..b8ef6d57fd 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -208,7 +208,7 @@ const result = MetadataBulkResultSchema.parse(data); | **executionPinned** | `boolean` | optional | Transaction rows reference a specific version_hash; history GC is disabled and getByHash() MUST resolve old hashes (ADR-0009) | | **loadOrder** | `integer` | optional | Loading priority (lower = earlier) | | **domain** | `Enum<'data' \| 'ui' \| 'automation' \| 'system' \| 'security' \| 'ai'>` | ✅ | Protocol domain | -| **actions** | `{ name: string; label: string; objectName?: string; icon?: string; … }[]` | optional | Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted | +| **actions** | `{ name: string; label: string \| Record; objectName?: string; icon?: string; … }[]` | optional | Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted | ### Allowed Values: `MetadataTypeRegistryEntry.type` diff --git a/content/docs/references/system/settings-manifest.mdx b/content/docs/references/system/settings-manifest.mdx index ee42c1f1f7..3d21718fa2 100644 --- a/content/docs/references/system/settings-manifest.mdx +++ b/content/docs/references/system/settings-manifest.mdx @@ -77,7 +77,7 @@ const result = ResolvedSettingValueSchema.parse(data); | :--- | :--- | :--- | :--- | | **namespace** | `string` | ✅ | Namespace (snake_case, globally unique) | | **version** | `integer` | optional | Manifest schema version | -| **label** | `string` | ✅ | Display label | +| **label** | `string \| Record` | ✅ | Display label | | **icon** | `string` | optional | Icon (Lucide) | | **description** | `string` | optional | Short description | | **helpText** | `string` | optional | Markdown help text shown above specifiers | @@ -86,7 +86,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **writePermission** | `string` | optional | Permission required to write | | **category** | `string` | optional | Settings hub category | | **order** | `number` | optional | Display order | -| **specifiers** | `{ type: Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| … +13 more>; id?: string; key?: string; label: string; … }[]` | ✅ | Page contents (ordered) | +| **specifiers** | `{ type: Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| … +13 more>; id?: string; key?: string; label: string \| Record; … }[]` | ✅ | Page contents (ordered) | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Whole-manifest visibility | | **featureFlag** | `string` | optional | Gate manifest visibility on a feature flag | | **beta** | `boolean` | optional | Show a Beta chip on the page | @@ -100,7 +100,7 @@ const result = ResolvedSettingValueSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **manifest** | `{ namespace: string; version?: integer; label: string; icon?: string; … }` | ✅ | | +| **manifest** | `{ namespace: string; version?: integer; label: string \| Record; icon?: string; … }` | ✅ | | | **values** | `Record; locked: boolean; lockedReason?: string; … }>` | ✅ | Effective values keyed by specifier.key | @@ -115,7 +115,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **type** | `Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| 'password' \| 'email' \| 'url' \| 'phone' \| 'number' \| 'toggle' \| 'select' \| 'radio' \| … +5 more>` | ✅ | Specifier variant | | **id** | `string` | optional | Stable identifier (snake_case) | | **key** | `string` | optional | Storage key (snake_case) | -| **label** | `string` | ✅ | Display label | +| **label** | `string \| Record` | ✅ | Display label | | **description** | `string` | optional | Help text | | **icon** | `string` | optional | Icon name (Lucide) | | **default** | `any` | optional | Default value | @@ -129,7 +129,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **writePermission** | `string` | optional | Permission required to write this specifier | | **deprecated** | `boolean` | optional | Mark deprecated | | **replacedBy** | `string` | optional | Replacement key (used when deprecated=true) | -| **options** | `{ value: string \| number \| boolean; label: string; description?: string; icon?: string }[]` | optional | Options for select/radio/multiselect | +| **options** | `{ value: string \| number \| boolean; label: string \| Record; description?: string; icon?: string }[]` | optional | Options for select/radio/multiselect | | **min** | `number` | optional | | | **max** | `number` | optional | | | **step** | `number` | optional | | @@ -137,7 +137,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **maxLength** | `integer` | optional | | | **pattern** | `string` | optional | Regex pattern (text only) | | **rows** | `integer` | optional | | -| **handler** | `{ kind: 'http'; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>; url: string; body?: Record; … } \| { kind: 'action'; name: string; params?: Record; confirmText?: string } \| { kind: 'navigate'; url: string; target?: Enum<'_self' \| '_blank'> }` | optional | Action handler (action_button) | +| **handler** | `{ kind: 'http'; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>; url: string; body?: Record; … } \| { kind: 'action'; name: string; params?: Record; confirmText?: string \| Record } \| { kind: 'navigate'; url: string; target?: Enum<'_self' \| '_blank'> }` | optional | Action handler (action_button) | | **childNamespace** | `string` | optional | Sub-namespace (child_pane) | | **bannerText** | `string` | optional | Markdown body (info_banner) | | **bannerSeverity** | `Enum<'info' \| 'success' \| 'warning' \| 'error'>` | optional | | @@ -183,7 +183,7 @@ This schema accepts one of the following structures: | **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>` | ✅ | | | **url** | `string` | ✅ | Endpoint URL; supports $`{...}` interpolation | | **body** | `Record` | optional | Optional JSON body; supports $`{...}` interpolation | -| **confirmText** | `string` | optional | Confirm dialog text before invoking (omit = no confirm) | +| **confirmText** | `string \| Record` | optional | Confirm dialog text before invoking (omit = no confirm) | --- @@ -196,7 +196,7 @@ This schema accepts one of the following structures: | **kind** | `'action'` | ✅ | | | **name** | `string` | ✅ | Registered action machine name | | **params** | `Record` | optional | | -| **confirmText** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **confirmText** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | --- @@ -222,7 +222,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **value** | `string \| number \| boolean` | ✅ | Stored value | -| **label** | `string` | ✅ | Display label | +| **label** | `string \| Record` | ✅ | Display label | | **description** | `string` | optional | Optional helper text | | **icon** | `string` | optional | Optional Lucide icon name | diff --git a/content/docs/references/system/translation.mdx b/content/docs/references/system/translation.mdx index 41115a4ab9..ecae7950d9 100644 --- a/content/docs/references/system/translation.mdx +++ b/content/docs/references/system/translation.mdx @@ -87,6 +87,7 @@ Translation data for a single object | **_views** | `Record` | optional | View translations keyed by view name | | **_actions** | `Record }>; … }>` | optional | Action translations keyed by action name | | **_sections** | `Record` | optional | Section translations keyed by section name | +| **_tabs** | `Record` | optional | Filter-preset tab translations keyed by tab name | --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 691ce611b4..49fc476b94 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -61,7 +61,7 @@ const result = ActionSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Machine name (lowercase snake_case) | -| **label** | `string` | ✅ | Display label | +| **label** | `string \| Record` | ✅ | Display label | | **objectName** | `string` | optional | Target object this action belongs to. When set, the action is auto-merged into the object's actions array by defineStack(). | | **icon** | `string` | optional | Icon name | | **locations** | `Enum<'list_toolbar' \| 'list_item' \| 'record_header' \| 'record_more' \| 'record_related' \| 'record_section' \| 'global_nav'>[]` | optional | Locations where this action is visible | @@ -71,15 +71,15 @@ const result = ActionSchema.parse(data); | **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). | | **body** | `{ language: 'expression'; source: string } \| { language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is `script`. | | **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | -| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string; … }[]` | optional | Input parameters required from user | +| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record; … }[]` | optional | Input parameters required from user | | **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) | | **order** | `number` | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order. | -| **confirmText** | `string` | optional | Confirmation message before execution | -| **successMessage** | `string` | optional | Success message to show after execution | -| **errorMessage** | `string` | optional | Error message to show when the action fails (overrides the raw error). | +| **confirmText** | `string \| Record` | optional | Confirmation message before execution | +| **successMessage** | `string \| Record` | optional | Success message to show after execution | +| **errorMessage** | `string \| Record` | optional | Error message to show when the action fails (overrides the raw error). | | **refreshAfter** | `boolean` | optional | Refresh view after execution | | **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. | -| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). | +| **resultDialog** | `{ title?: string \| Record; description?: string \| Record; acknowledge?: string \| Record; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). | | **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is offered when it evaluates TRUE. Omit = always visible. | | **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| … +3 more>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. | | **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. | @@ -95,7 +95,7 @@ const result = ActionSchema.parse(data); | **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. | | **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | | **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | | **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | @@ -163,10 +163,10 @@ const result = ActionSchema.parse(data); | **name** | `string` | optional | | | **field** | `string` | optional | Snake case identifier (lowercase with underscores only) | | **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | optional | | | **required** | `boolean` | optional | | -| **options** | `{ label: string; value: string; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }[]` | optional | | +| **options** | `{ label: string \| Record; value: string; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }[]` | optional | | | **placeholder** | `string` | optional | | | **helpText** | `string` | optional | | | **defaultValue** | `any` | optional | | @@ -271,14 +271,14 @@ const result = ActionSchema.parse(data); | :--- | :--- | :--- | :--- | | **type** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional | Action functionality type | | **name** | `string` | optional | Machine name (lowercase snake_case) | -| **label** | `string` | optional | Display label | +| **label** | `string \| Record` | optional | Display label | | **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. | | **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). | | **method** | `Enum<'POST' \| 'PATCH' \| 'PUT' \| 'DELETE'>` | optional | HTTP method for type:"api" actions. Defaults to POST. | -| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string; … }[]` | optional | Input parameters required from user | -| **confirmText** | `string` | optional | Confirmation message before execution | -| **successMessage** | `string` | optional | Success message to show after execution | -| **errorMessage** | `string` | optional | Error message to show when the action fails (overrides the raw error). | +| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record; … }[]` | optional | Input parameters required from user | +| **confirmText** | `string \| Record` | optional | Confirmation message before execution | +| **successMessage** | `string \| Record` | optional | Success message to show after execution | +| **errorMessage** | `string \| Record` | optional | Error message to show when the action fails (overrides the raw error). | | **refreshAfter** | `boolean` | optional | Refresh view after execution | | **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 6987dd2b8b..8fe4c05220 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -43,7 +43,7 @@ const result = ActionNavItemSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -65,17 +65,17 @@ const result = ActionNavItemSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | App unique machine name (lowercase snake_case) | -| **label** | `string` | ✅ | App display label | +| **label** | `string \| Record` | ✅ | App display label | | **version** | `never` | optional | [REMOVED] `App.version` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no consumer in framework or objectui). An app is versioned by its owning package: use `manifest.version`. Delete the key. | -| **description** | `string` | optional | App description | +| **description** | `string \| Record` | optional | App description | | **icon** | `string` | optional | App icon used in the App Launcher | | **branding** | `{ primaryColor?: string; accentColor?: string; logo?: string; favicon?: string }` | optional | App-specific branding | | **active** | `boolean` | optional | Whether the app is enabled | | **isDefault** | `boolean` | optional | Is default app | | **hidden** | `boolean` | optional | Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead | -| **navigation** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[]` | optional | Full navigation tree for the app sidebar | -| **areas** | `{ id: string; label: string; icon?: string; description?: string; … }[]` | optional | Navigation areas for partitioning navigation by business domain | -| **contextSelectors** | `{ id: string; label: string; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{}` template vars | +| **navigation** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| … +5 more)[]` | optional | Full navigation tree for the app sidebar | +| **areas** | `{ id: string; label: string \| Record; icon?: string; description?: string \| Record; … }[]` | optional | Navigation areas for partitioning navigation by business domain | +| **contextSelectors** | `{ id: string; label: string \| Record; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{}` template vars | | **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **requiredPermissions** | `string[]` | optional | Permissions required to access this app | | **objects** | `never` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. | @@ -118,7 +118,7 @@ const result = ActionNavItemSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Selector id; selected value is exposed as the nav template var `{}` | -| **label** | `string` | ✅ | Dropdown label | +| **label** | `string \| Record` | ✅ | Dropdown label | | **icon** | `string` | optional | Icon name | | **optionsSource** | `{ endpoint: string; valueKey: string; labelKey: string; filter?: { key: string; op: Enum<'eq' \| 'ne' \| 'in' \| 'nin'>; value: string \| string[] }[] }` | ✅ | Option data source | | **allValue** | `string` | ✅ | Sentinel value meaning "no concrete selection yet" (empty string is almost always right) | @@ -134,7 +134,7 @@ const result = ActionNavItemSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -157,7 +157,7 @@ const result = ActionNavItemSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -179,7 +179,7 @@ const result = ActionNavItemSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -201,10 +201,10 @@ const result = ActionNavItemSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique area identifier (lowercase snake_case) | -| **label** | `string` | ✅ | Area display label | +| **label** | `string \| Record` | ✅ | Area display label | | **icon** | `string` | optional | Area icon name | -| **description** | `string` | optional | Area description | -| **navigation** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items within this area | +| **description** | `string \| Record` | optional | Area description | +| **navigation** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items within this area | --- @@ -220,7 +220,7 @@ A navigation contribution: a package injecting nav items into an app it does not | **app** | `string` | ✅ | Target app name to contribute navigation into (e.g. "setup") | | **group** | `string` | optional | Target group nav-item id to append into (e.g. "group_integrations"); omit to append at the app top level | | **priority** | `integer` | optional | Merge priority within the target group — lower applied first (matches object extender priority) | -| **items** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items contributed into the target app/group | +| **items** | `({ id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| { id: string; label: string \| Record; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items contributed into the target app/group | --- @@ -240,7 +240,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -268,7 +268,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -291,7 +291,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -315,7 +315,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -339,7 +339,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -362,7 +362,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -385,7 +385,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -423,7 +423,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -448,7 +448,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -474,7 +474,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -497,7 +497,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | @@ -519,7 +519,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique identifier for this navigation item (lowercase snake_case) | -| **label** | `string` | ✅ | Display proper label | +| **label** | `string \| Record` | ✅ | Display proper label | | **icon** | `string` | optional | Icon name | | **order** | `number` | optional | Sort order within the same level (lower = first) | | **badge** | `string \| number` | optional | Badge text or count displayed on the item | diff --git a/content/docs/references/ui/chart.mdx b/content/docs/references/ui/chart.mdx index 9645e8ccab..f70c81954d 100644 --- a/content/docs/references/ui/chart.mdx +++ b/content/docs/references/ui/chart.mdx @@ -65,7 +65,7 @@ Inline aggregation for an object-bound chart | **value** | `number \| string` | ✅ | Start value | | **endValue** | `number \| string` | optional | End value for regions | | **color** | `string` | optional | | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **style** | `Enum<'solid' \| 'dashed' \| 'dotted'>` | ✅ | | @@ -78,7 +78,7 @@ Inline aggregation for an object-bound chart | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **field** | `string` | ✅ | Data field key | -| **title** | `string` | optional | Axis display title | +| **title** | `string \| Record` | optional | Axis display title | | **format** | `string` | optional | Value format string (e.g., "$0,0.00") | | **min** | `number` | optional | Minimum value | | **max** | `number` | optional | Maximum value | @@ -97,19 +97,19 @@ Inline aggregation for an object-bound chart | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | ✅ | | -| **title** | `string` | optional | Chart title | -| **subtitle** | `string` | optional | Chart subtitle | -| **description** | `string` | optional | Accessibility description — announced to screen readers as the chart’s label | -| **xAxis** | `{ field: string; title?: string; format?: string; min?: number; … }` | optional | X-Axis configuration | -| **yAxis** | `{ field: string; title?: string; format?: string; min?: number; … }[]` | optional | Y-Axis configuration (support dual axis) | -| **series** | `{ name: string; label?: string; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; color?: string; … }[]` | optional | Defined series configuration | +| **title** | `string \| Record` | optional | Chart title | +| **subtitle** | `string \| Record` | optional | Chart subtitle | +| **description** | `string \| Record` | optional | Accessibility description — announced to screen readers as the chart’s label | +| **xAxis** | `{ field: string; title?: string \| Record; format?: string; min?: number; … }` | optional | X-Axis configuration | +| **yAxis** | `{ field: string; title?: string \| Record; format?: string; min?: number; … }[]` | optional | Y-Axis configuration (support dual axis) | +| **series** | `{ name: string; label?: string \| Record; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; color?: string; … }[]` | optional | Defined series configuration | | **colors** | `string[] \| Record` | optional | Color palette (string[]) or value→color map (`{ value: color }`) | | **height** | `number` | optional | Fixed plot height in pixels (overrides the container default) | | **showLegend** | `boolean` | ✅ | Display legend | | **showDataLabels** | `boolean` | ✅ | Display data labels | | **annotations** | `{ type: Enum<'line' \| 'region'>; axis: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` | | **interaction** | `{ tooltips: boolean; brush: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | ### Allowed Values: `ChartConfig.type` @@ -201,7 +201,7 @@ Type: `string` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Field name or series identifier | -| **label** | `string` | optional | Series display label | +| **label** | `string \| Record` | optional | Series display label | | **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | optional | Override chart type for this series | | **color** | `string` | optional | Series color (hex/rgb/token) | | **stack** | `string` | optional | Stack identifier to group series | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 4748dfb39c..35ac7b6c99 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -32,7 +32,7 @@ const result = AIChatWindowProps.parse(data); | **mode** | `Enum<'float' \| 'sidebar' \| 'inline'>` | ✅ | Display mode for the chat window | | **agentId** | `string` | optional | Specific AI agent to use | | **context** | `Record` | optional | Contextual data to pass to the AI | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -43,14 +43,14 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **label** | `string` | ✅ | Button display label | +| **label** | `string \| Record` | ✅ | Button display label | | **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant | | **size** | `Enum<'small' \| 'medium' \| 'large'>` | optional | Button size | | **icon** | `string` | optional | Icon name (Lucide icon) | | **iconPosition** | `Enum<'left' \| 'right'>` | optional | Icon position relative to label | | **disabled** | `boolean` | optional | Disable the button | -| **action** | `{ type?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; name?: string; label?: string; target?: string; … }` | optional | Inline action executed on click | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **action** | `{ type?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; name?: string; label?: string \| Record; target?: string; … }` | optional | Inline action executed on click | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -66,7 +66,7 @@ const result = AIChatWindowProps.parse(data); | **targetVariable** | `string` | optional | Page variable to store filter state | | **layout** | `Enum<'inline' \| 'dropdown' \| 'sidebar'>` | ✅ | Filter display layout | | **showSearch** | `boolean` | ✅ | Show search input | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -80,9 +80,9 @@ const result = AIChatWindowProps.parse(data); | **object** | `string` | ✅ | Object for the form | | **fields** | `string[]` | optional | Fields to display (defaults to all editable fields) | | **mode** | `Enum<'create' \| 'edit'>` | optional | Form mode | -| **submitLabel** | `string` | optional | Submit button label | +| **submitLabel** | `string \| Record` | optional | Submit button label | | **onSubmit** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Action expression on form submit (CEL) | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -97,7 +97,7 @@ const result = AIChatWindowProps.parse(data); | **alt** | `string` | optional | Alt text for accessibility | | **fit** | `Enum<'cover' \| 'contain' \| 'fill'>` | ✅ | Image object-fit mode | | **height** | `number` | optional | Fixed height in pixels | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -113,7 +113,7 @@ const result = AIChatWindowProps.parse(data); | **object** | `string` | optional | Owning object — required for object-scoped kinds: state_machine is a rule ON an object (ADR-0020), permission renders a matrix FOR one; omit for top-level flow | | **mode** | `Enum<'diagram' \| 'matrix' \| 'summary'>` | optional | Render form; defaults per type (diagram for flow/state_machine, matrix for permission) | | **detail** | `Enum<'business' \| 'technical'>` | ✅ | Authoring altitude (ADR-0051 §3.4): business collapses technical flow nodes to business steps + approvals. NOT access (cf. book.audience); permission projection is automatic and render-time, never set here | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -131,7 +131,7 @@ const result = AIChatWindowProps.parse(data); | **format** | `Enum<'number' \| 'currency' \| 'percent'>` | optional | Number display format | | **prefix** | `string` | optional | Prefix text (e.g. "$") | | **suffix** | `string` | optional | Suffix text (e.g. "%") | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -145,15 +145,15 @@ const result = AIChatWindowProps.parse(data); | **object** | `string` | ✅ | Object to pick records from | | **labelField** | `string` | optional | Field rendered as each row's text (default `name`) | | **valueField** | `string` | optional | Field whose value is written into the bound page variable (default `id`) | -| **label** | `string` | optional | Control label rendered above the select | +| **label** | `string \| Record` | optional | Control label rendered above the select | | **filter** | `any` | optional | Filter criteria for available records | | **targetVariable** | `string` | optional | Page variable to bind selected record ID(s) | -| **placeholder** | `string` | optional | Placeholder text | -| **emptyText** | `string` | optional | Text shown when the query returns no records (default "No records") | +| **placeholder** | `string \| Record` | optional | Placeholder text | +| **emptyText** | `string \| Record` | optional | Text shown when the query returns no records (default "No records") | | **displayField** | `never` | optional | [REMOVED] `element:record_picker` property `displayField` was removed in @objectstack/spec 17.0.0 (#5775, ADR-0087 D2) — it was a required declaration no renderer ever read, while the renderer honoured `labelField` for the same thing and defaulted to `name`. Rename the key to `labelField`; the value (a field name) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | | **searchFields** | `never` | optional | [REMOVED] `element:record_picker` property `searchFields` was removed in @objectstack/spec 17.0.0 (#5775, ADR-0049) — the picker renders a plain single-select with no search input, so no renderer ever read it and it narrowed nothing. Delete the key. To restrict which records the picker offers, use `filter` (or the component-level `dataSource.filter`), which the query path does apply. Run `os migrate meta --from 16` to remove it automatically. | | **multiple** | `never` | optional | [REMOVED] `element:record_picker` property `multiple` was removed in @objectstack/spec 17.0.0 (#5775, ADR-0049) — the picker is a single-select `Select` and the bound page variable holds one record id, so `multiple: true` selected nothing extra and reported success. Delete the key; multi-record selection is not implemented on this element. Run `os migrate meta --from 16` to remove it automatically. | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -165,14 +165,14 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **inputType** | `Enum<'text' \| 'email' \| 'number' \| 'tel' \| 'url' \| 'password'>` | ✅ | Native input type — drives keyboard/validation affordance and how the bound value is coerced (number → numeric). | -| **label** | `string` | optional | Field label shown above the input | -| **placeholder** | `string` | optional | Placeholder text shown when empty | +| **label** | `string \| Record` | optional | Field label shown above the input | +| **placeholder** | `string \| Record` | optional | Placeholder text shown when empty | | **defaultValue** | `string \| number` | optional | Initial value; seeds the bound page variable on mount | | **required** | `boolean` | ✅ | Mark the field as required | | **disabled** | `boolean` | ✅ | Disable the input | -| **description** | `string` | optional | Helper text shown below the input | +| **description** | `string \| Record` | optional | Helper text shown below the input | | **targetVariable** | `string` | optional | Page variable this input writes to. Declarative hint; the live binding resolves via the variable whose `source` equals this component id (see PageVariableSchema). | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -183,10 +183,10 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **content** | `string` | ✅ | Text or Markdown content | +| **content** | `string \| Record` | ✅ | Text or Markdown content — a plain string, or an inline locale map | | **variant** | `Enum<'heading' \| 'subheading' \| 'body' \| 'caption'>` | ✅ | Text style variant | | **align** | `Enum<'left' \| 'center' \| 'right'>` | ✅ | Text alignment | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -197,9 +197,9 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **items** | `{ label: string; icon?: string; collapsed: boolean; children: any[] }[]` | ✅ | | +| **items** | `{ label: string \| Record; icon?: string; collapsed: boolean; children: any[] }[]` | ✅ | | | **allowMultiple** | `boolean` | ✅ | Allow multiple panels to be expanded simultaneously | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -210,13 +210,13 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **title** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **title** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **bordered** | `boolean` | ✅ | | | **actions** | `string[]` | optional | | | **children** | `any[]` | optional | Card content components, in order (the card body slot) | | **body** | `never` | optional | [REMOVED] `page:card` property `body` was removed in @objectstack/spec 17.0.0 (#5775, ADR-0087 D2) — it was a second spelling of the composition slot every other container calls `children`, and the renderer reads both. Rename the key to `children`; the value (an array of child components) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | | **footer** | `any[]` | optional | Card footer components (slot) | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -238,12 +238,12 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **title** | `string` | ✅ | Page title | -| **subtitle** | `string` | optional | Page subtitle | +| **title** | `string \| Record` | ✅ | Page title | +| **subtitle** | `string \| Record` | optional | Page subtitle | | **icon** | `string` | optional | Icon name | | **breadcrumb** | `boolean` | ✅ | Show breadcrumb | | **actions** | `string[]` | optional | Action IDs to show in header | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -256,8 +256,8 @@ const result = AIChatWindowProps.parse(data); | :--- | :--- | :--- | :--- | | **type** | `Enum<'line' \| 'card' \| 'pill'>` | optional | | | **position** | `Enum<'top' \| 'left'>` | optional | | -| **items** | `{ label: string; icon?: string; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; value?: string; … }[]` | ✅ | | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **items** | `{ label: string \| Record; icon?: string; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; value?: string; … }[]` | ✅ | | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -279,7 +279,7 @@ const result = AIChatWindowProps.parse(data); | **enableReactions** | `boolean` | ✅ | Enable emoji reactions on feed items | | **enableThreading** | `boolean` | ✅ | Enable threaded replies on comments | | **showSubscriptionToggle** | `boolean` | ✅ | Show bell icon for record-level notification subscription | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -295,7 +295,7 @@ const result = AIChatWindowProps.parse(data); | **collapsible** | `boolean` | ✅ | Whether the panel can be collapsed | | **defaultCollapsed** | `boolean` | ✅ | Whether the panel starts collapsed | | **feed** | `{ types?: Enum<'comment' \| 'field_change' \| 'task' \| 'event' \| 'email' \| 'call' \| 'note' \| … +6 more>[]; filterMode: Enum<'all' \| 'comments_only' \| 'changes_only' \| 'tasks_only'>; showFilterToggle: boolean; limit: integer; … }` | optional | Embedded activity feed configuration | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -308,10 +308,10 @@ const result = AIChatWindowProps.parse(data); | :--- | :--- | :--- | :--- | | **columns** | `Enum<'1' \| '2' \| '3' \| '4'>` | ✅ | Number of columns for field layout (1-4) | | **layout** | `Enum<'auto' \| 'custom'>` | ✅ | Layout mode: auto uses object highlightFields, custom uses explicit sections | -| **sections** | `{ name?: string; label?: string; columns?: integer; fields: string[] }[]` | optional | Field groups rendered as the detail body, in order. Object form: `{ name?, label?, columns?, fields }`. | +| **sections** | `{ name?: string; label?: string \| Record; columns?: integer; fields: string[] }[]` | optional | Field groups rendered as the detail body, in order. Object form: `{ name?, label?, columns?, fields }`. | | **fields** | `string[]` | optional | Explicit field list to display (optional, overrides highlightFields) | | **hideFields** | `string[]` | optional | Field names to omit from the body — applied to `fields` and to every section's `fields` (used to dedupe fields already shown in `record:highlights` or as the page title) | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -355,7 +355,7 @@ Type: `string` | :--- | :--- | :--- | :--- | | **fields** | `(string \| { name: string; label?: string; icon?: string; type?: string; … })[]` | ✅ | Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or `{name, label?, icon?, type?, readonly?}` for inline overrides. | | **layout** | `Enum<'horizontal' \| 'vertical'>` | ✅ | Layout orientation for highlight fields | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -367,8 +367,8 @@ Type: `string` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **statusField** | `string` | ✅ | Field name representing the current status/stage | -| **stages** | `{ value: string; label: string; terminal?: Enum<'won' \| 'lost'> }[]` | optional | Explicit stage definitions (if not using field metadata) | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **stages** | `{ value: string; label: string \| Record; terminal?: Enum<'won' \| 'lost'> }[]` | optional | Explicit stage definitions (if not using field metadata) | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -386,11 +386,11 @@ Type: `string` | **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order for related records | | **limit** | `integer` | ✅ | Number of records to display initially | | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| … +14 more>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Additional filter criteria for related records | -| **title** | `string` | optional | Custom title for the related list | +| **title** | `string \| Record` | optional | Custom title for the related list | | **showViewAll** | `boolean` | ✅ | Show "View All" link to see all related records | | **actions** | `string[]` | optional | Action IDs available for related records | -| **add** | `{ picker: object; linkField?: string; label?: string }` | optional | Add-existing-via-picker config (generic m2m/junction assignment). | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **add** | `{ picker: object; linkField?: string; label?: string \| Record }` | optional | Add-existing-via-picker config (generic m2m/junction assignment). | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index 9dfc88c073..b7430a97d9 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -30,15 +30,15 @@ const result = DashboardSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Dashboard unique name | -| **label** | `string` | ✅ | Dashboard label | -| **description** | `string` | optional | Dashboard description | -| **header** | `{ showTitle: boolean; showDescription: boolean; actions?: { label: string; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[] }` | optional | Dashboard header configuration | -| **widgets** | `{ id: string; title?: string; description?: string; type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; … }[]` | ✅ | Widgets to display | +| **label** | `string \| Record` | ✅ | Dashboard label | +| **description** | `string \| Record` | optional | Dashboard description | +| **header** | `{ showTitle: boolean; showDescription: boolean; actions?: { label: string \| Record; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[] }` | optional | Dashboard header configuration | +| **widgets** | `{ id: string; title?: string \| Record; description?: string \| Record; type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; … }[]` | ✅ | Widgets to display | | **columns** | `integer` | optional | Number of grid columns (default 12) | | **gap** | `integer` | optional | Grid gap in Tailwind spacing units | | **refreshInterval** | `number` | optional | Auto-refresh interval in seconds | | **dateRange** | `{ field?: string; defaultRange: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| … +8 more>; allowCustomRange: boolean }` | optional | Global dashboard date range filter configuration | -| **globalFilters** | `{ name?: string; field: string; label?: string; type?: Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>; … }[]` | optional | Global filters that apply to all widgets in the dashboard | +| **globalFilters** | `{ name?: string; field: string; label?: string \| Record; type?: Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>; … }[]` | optional | Global filters that apply to all widgets in the dashboard | | **aria** | `never` | optional | [REMOVED] `dashboard.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no dashboard renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. | | **performance** | `never` | optional | [REMOVED] `dashboard.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; dashboard performance tuning was never implemented. Delete the key. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this dashboard. | @@ -63,7 +63,7 @@ Dashboard header configuration | :--- | :--- | :--- | :--- | | **showTitle** | `boolean` | ✅ | Show dashboard title in header | | **showDescription** | `boolean` | ✅ | Show dashboard description in header | -| **actions** | `{ label: string; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[]` | optional | Header action buttons | +| **actions** | `{ label: string \| Record; actionUrl: string; actionType?: Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>; icon?: string }[]` | optional | Header action buttons | --- @@ -76,7 +76,7 @@ Dashboard header action | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **label** | `string` | ✅ | Action button label | +| **label** | `string \| Record` | ✅ | Action button label | | **actionUrl** | `string` | ✅ | URL or target for the action | | **actionType** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api' \| 'form'>` | optional | Type of action | | **icon** | `string` | optional | Icon identifier for the action button | @@ -91,10 +91,10 @@ Dashboard header action | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique widget identifier (snake_case) | -| **title** | `string` | optional | Widget title | -| **description** | `string` | optional | Widget description text below the header | +| **title** | `string \| Record` | optional | Widget title | +| **description** | `string \| Record` | optional | Widget description text below the header | | **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | ✅ | Visualization type | -| **chartConfig** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; title?: string; subtitle?: string; description?: string; … }` | optional | Chart visualization configuration | +| **chartConfig** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; title?: string \| Record; subtitle?: string \| Record; description?: string \| Record; … }` | optional | Chart visualization configuration | | **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming | | **requiresObject** | `string` | optional | Hide the widget unless the named object is registered | | **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered | @@ -164,9 +164,9 @@ Widget configuration — declared query keys + open renderer extras | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Stable filter name (variable key); defaults to field | | **field** | `string` | ✅ | Field name to filter on | -| **label** | `string` | optional | Display label for the filter | +| **label** | `string \| Record` | optional | Display label for the filter | | **type** | `Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>` | optional | Filter input type | -| **options** | `{ value: string \| number \| boolean; label: string }[]` | optional | Static filter options | +| **options** | `{ value: string \| number \| boolean; label: string \| Record }[]` | optional | Static filter options | | **optionsFrom** | `{ object: string; valueField: string; labelField: string; filter?: any }` | optional | Dynamic filter options from object | | **defaultValue** | `string \| number \| boolean` | optional | Default filter value | | **scope** | `Enum<'dashboard' \| 'widget'>` | ✅ | Filter application scope | diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index 265449a2de..1ca1dbbf9d 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -49,13 +49,13 @@ const result = DatasetSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Dataset unique name | -| **label** | `string` | ✅ | Dataset label | -| **description** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | ✅ | Dataset label | +| **description** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **object** | `string` | ✅ | Base object name | | **include** | `string[]` | optional | Relationship names/paths to join (derived from object graph; max 3 hops) | | **filter** | `any` | optional | Intrinsic dataset scope filter | -| **dimensions** | `{ name: string; label?: string; field: string; type?: Enum<'string' \| 'number' \| 'date' \| 'boolean' \| 'lookup'>; … }[]` | ✅ | Groupable axes | -| **measures** | `{ name: string; label?: string; aggregate?: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; … }[]` | ✅ | Aggregatable values | +| **dimensions** | `{ name: string; label?: string \| Record; field: string; type?: Enum<'string' \| 'number' \| 'date' \| 'boolean' \| 'lookup'>; … }[]` | ✅ | Groupable axes | +| **measures** | `{ name: string; label?: string \| Record; aggregate?: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; … }[]` | ✅ | Aggregatable values | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this dataset. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | @@ -75,7 +75,7 @@ const result = DatasetSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Dimension name — referenced by presentations | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **field** | `string` | ✅ | Base field, or `relationship[.relationship].field` path | | **type** | `Enum<'string' \| 'number' \| 'date' \| 'boolean' \| 'lookup'>` | optional | | | **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | | @@ -90,7 +90,7 @@ const result = DatasetSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Measure name — e.g. "revenue"; defined once | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>` | optional | Aggregation (sum/avg/count/...); omit when `derived` is set | | **field** | `string` | optional | Aggregated field; optional for count(*) | | **filter** | `any` | optional | | diff --git a/content/docs/references/ui/i18n.mdx b/content/docs/references/ui/i18n.mdx index 048c0d58c0..922b29cede 100644 --- a/content/docs/references/ui/i18n.mdx +++ b/content/docs/references/ui/i18n.mdx @@ -12,8 +12,8 @@ description: I18n protocol schemas ## TypeScript Usage ```typescript -import { AriaPropsSchema, I18nLabelSchema } from '@objectstack/spec/ui'; -import type { AriaProps, I18nLabel } from '@objectstack/spec/ui'; +import { AriaPropsSchema, I18nLabelSchema, InlineLocaleMapSchema } from '@objectstack/spec/ui'; +import type { AriaProps, I18nLabel, InlineLocaleMap } from '@objectstack/spec/ui'; // Validate data const result = AriaPropsSchema.parse(data); @@ -29,11 +29,36 @@ ARIA accessibility attributes | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **ariaLabel** | `string` | optional | Accessible label for screen readers (WAI-ARIA aria-label) | +| **ariaLabel** | `string \| Record` | optional | Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — no translation-bundle slot addresses this key, so a plain string is announced in the source language. | | **ariaDescribedBy** | `string` | optional | ID of element providing additional description (WAI-ARIA aria-describedby) | | **role** | `string` | optional | WAI-ARIA role attribute (e.g., "dialog", "navigation", "alert") | +--- + +## I18nLabel + +Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time + +### Union Options + +This schema accepts one of the following structures: + +#### Option 1 + +Type: `string` + +--- + +#### Option 2 + +Inline locale map: BCP-47 tag → translated string + +Type: `Record` + +--- + + --- diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index d055e8f864..d9985bac89 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -48,13 +48,13 @@ Interface-level page configuration (Airtable parity) | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **source** | `string` | optional | Source object name for the page | -| **columns** | `string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Columns shown by the page. Blank = all object fields. Defined directly on the page (no view inheritance). | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Columns shown by the page. Blank = all object fields. Defined directly on the page (no view inheritance). | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Default sort order for the page, defined directly on the page. | | **filterBy** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| … +14 more>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Always-on page filter (base filter). | | **levels** | `integer` | optional | Number of hierarchy levels to display | | **sourceView** | `string` | optional | @deprecated Legacy named-view inheritance. Define columns/sort/filterBy on the page instead. | | **appearance** | `{ showDescription: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | -| **userFilters** | `{ element: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: { field: string; label?: string; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string; color?: string }[]; … }[]; tabs?: { name: string; label?: string; icon?: string; view?: string; … }[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar for this page (overrides the source view's userFilters) | +| **userFilters** | `{ element: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: { field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string \| Record; color?: string }[]; … }[]; tabs?: { name: string; label?: string \| Record; icon?: string; view?: string; … }[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar for this page (overrides the source view's userFilters) | | **userActions** | `{ sort: boolean; search: boolean; filter: boolean; refresh: boolean; … }` | optional | User action toggles | | **addRecord** | `{ enabled: boolean; position: Enum<'top' \| 'bottom' \| 'both'>; mode: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | | **buttons** | `string[]` | optional | Toolbar buttons — names of the source object's actions to surface in the page toolbar | @@ -72,20 +72,20 @@ Interface-level page configuration (Airtable parity) | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Page unique name (lowercase snake_case) | -| **label** | `string` | ✅ | Display label (plain string; i18n keys are auto-generated by the framework) | -| **description** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | ✅ | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **description** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **icon** | `string` | optional | Page icon name | | **type** | `Enum<'record' \| 'home' \| 'app' \| 'utility' \| 'list'>` | optional | Page type | | **variables** | `{ name: string; type?: Enum<'string' \| 'number' \| 'boolean' \| 'object' \| 'array' \| 'record_id'>; defaultValue?: any; source?: string }[]` | optional | Local page state, exposed to expressions as `page.` and writable by interactive elements via `source` (master/detail, filtered dashboards). | | **object** | `string` | optional | Bound object (for Record pages) | | **template** | `string` | optional | Layout template name (e.g. "header-sidebar-main") | -| **regions** | `{ name: string; width?: Enum<'small' \| 'medium' \| 'large' \| 'full'>; components: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … }[] }[]` | optional | Layout regions (header, main, sidebar, footer) with their components. Optional — list pages use interfaceConfig, slotted pages use slots, and an empty full page falls back to the synthesized default layout. | +| **regions** | `{ name: string; width?: Enum<'small' \| 'medium' \| 'large' \| 'full'>; components: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … }[] }[]` | optional | Layout regions (header, main, sidebar, footer) with their components. Optional — list pages use interfaceConfig, slotted pages use slots, and an empty full page falls back to the synthesized default layout. | | **isDefault** | `boolean` | optional | | | **assignedProfiles** | `string[]` | optional | | -| **interfaceConfig** | `{ source?: string; columns?: string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]; sort?: { field: string; order: Enum<'asc' \| 'desc'> }[]; filterBy?: { field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| … +14 more>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **interfaceConfig** | `{ source?: string; columns?: string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]; sort?: { field: string; order: Enum<'asc' \| 'desc'> }[]; filterBy?: { field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| … +14 more>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | | **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react' \| 'jsx'>` | optional | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX/HTML+Tailwind compiled (parsed, never executed) to the tree (ADR-0080; the legacy value 'jsx' is a deprecated alias); react = real-React source executed at render by the runtime (ADR-0081); it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. | -| **slots** | `{ header?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … }[]; actions?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … }[]; alerts?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … }[]; highlights?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … }[]; … }` | optional | Slot override map for slotted pages | +| **slots** | `{ header?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]; actions?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]; alerts?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]; highlights?: { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]; … }` | optional | Slot override map for slotted pages | | **source** | `string` | optional | Page source text. For kind==='html' (alias 'jsx') it is constrained JSX/HTML+Tailwind compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute). For kind==='react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier). Authoritative over `regions` in both. | | **requires** | `string[]` | optional | Plugin namespaces the JSX source references (validated at save and load) | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -107,7 +107,7 @@ Interface-level page configuration (Airtable parity) | :--- | :--- | :--- | :--- | | **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) | | **id** | `string` | optional | Unique instance ID | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **properties** | `Record` | optional | Component props passed to the widget. See component.zod.ts for schemas. | | **events** | `Record` | optional | Event handlers map | | **style** | `Record` | optional | Inline styles or utility classes | @@ -117,7 +117,7 @@ Interface-level page configuration (Airtable parity) | **visibility** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. | | **dataSource** | `{ object: string; view?: string; filter?: any; sort?: { field: string; order: Enum<'asc' \| 'desc'> }[]; … }` | optional | Per-element data binding for multi-object pages | | **responsive** | `{ breakpoint?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>; hiddenOn?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>[]; columns?: object; order?: object }` | optional | Responsive layout configuration | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | --- @@ -172,7 +172,7 @@ Interface-level page configuration (Airtable parity) | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Region name (e.g. "sidebar", "main", "header") | | **width** | `Enum<'small' \| 'medium' \| 'large' \| 'full'>` | optional | | -| **components** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string; properties?: Record; … }[]` | ✅ | Components in this region | +| **components** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| … +29 more> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]` | ✅ | Components in this region | --- diff --git a/content/docs/references/ui/report.mdx b/content/docs/references/ui/report.mdx index 8174b4c02b..12f3a6b0d4 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -30,10 +30,10 @@ const result = JoinedReportBlockSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Snake case identifier (lowercase with underscores only) | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | -| **description** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | +| **description** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **type** | `Enum<'tabular' \| 'summary' \| 'matrix'>` | ✅ | | -| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; title?: string; subtitle?: string; description?: string; … }` | optional | | +| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; title?: string \| Record; subtitle?: string \| Record; description?: string \| Record; … }` | optional | | | **dataset** | `string` | optional | Dataset name to bind (ADR-0021) | | **rows** | `string[]` | optional | Dimension names down (dataset-bound) | | **columns** | `string[]` | optional | Dimension names across (matrix, dataset-bound) | @@ -51,8 +51,8 @@ const result = JoinedReportBlockSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Report unique name | -| **label** | `string` | ✅ | Report label | -| **description** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | ✅ | Report label | +| **description** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **type** | `Enum<'tabular' \| 'summary' \| 'matrix' \| 'joined'>` | ✅ | Report format type | | **dataset** | `string` | optional | Dataset name to bind (ADR-0021) | | **rows** | `string[]` | optional | Dimension names down | @@ -61,8 +61,8 @@ const result = JoinedReportBlockSchema.parse(data); | **runtimeFilter** | `any` | optional | Render-time scope filter | | **order** | `{ by: string; direction: Enum<'asc' \| 'desc'> }[]` | optional | Result ordering, most significant key first | | **drilldown** | `boolean` | ✅ | Click-through to underlying records | -| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; title?: string; subtitle?: string; description?: string; … }` | optional | Embedded chart configuration | -| **blocks** | `{ name: string; label?: string; description?: string; type: Enum<'tabular' \| 'summary' \| 'matrix'>; … }[]` | optional | Sub-reports for type=joined | +| **chart** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; title?: string \| Record; subtitle?: string \| Record; description?: string \| Record; … }` | optional | Embedded chart configuration | +| **blocks** | `{ name: string; label?: string \| Record; description?: string \| Record; type: Enum<'tabular' \| 'summary' \| 'matrix'>; … }[]` | optional | Sub-reports for type=joined | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this report. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | @@ -82,19 +82,19 @@ const result = JoinedReportBlockSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | ✅ | | -| **title** | `string` | optional | Chart title | -| **subtitle** | `string` | optional | Chart subtitle | -| **description** | `string` | optional | Accessibility description — announced to screen readers as the chart’s label | +| **title** | `string \| Record` | optional | Chart title | +| **subtitle** | `string \| Record` | optional | Chart subtitle | +| **description** | `string \| Record` | optional | Accessibility description — announced to screen readers as the chart’s label | | **xAxis** | `string` | ✅ | Dataset dimension name for the X-axis (bound-dataset dimension, not a raw field) | | **yAxis** | `string` | ✅ | Dataset measure name for the Y-axis (bound-dataset measure, not a raw field) | -| **series** | `{ name: string; label?: string; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; color?: string; … }[]` | optional | Defined series configuration | +| **series** | `{ name: string; label?: string \| Record; type?: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; color?: string; … }[]` | optional | Defined series configuration | | **colors** | `string[] \| Record` | optional | Color palette (string[]) or value→color map (`{ value: color }`) | | **height** | `number` | optional | Fixed plot height in pixels (overrides the container default) | | **showLegend** | `boolean` | ✅ | Display legend | | **showDataLabels** | `boolean` | ✅ | Display data labels | | **annotations** | `{ type: Enum<'line' \| 'region'>; axis: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` | | **interaction** | `{ tooltips: boolean; brush: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | ### Allowed Values: `ReportChart.type` diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 5240493b32..50b60fa926 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -124,7 +124,7 @@ Column footer summary configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **show** | `boolean` | optional | Whether the button is rendered (renderer default applies when omitted) | -| **label** | `string` | optional | Button label (i18n-capable; renderer default when omitted) | +| **label** | `string \| Record` | optional | Button label (i18n-capable; renderer default when omitted) | --- @@ -146,9 +146,9 @@ Column footer summary configuration | **precision** | `number` | optional | Total digits (for number/currency) | | **scale** | `number` | optional | Decimal places | | **multiple** | `boolean` | optional | Allow multiple values (for select/lookup/file/image) | -| **label** | `string` | optional | Display label override | -| **placeholder** | `string` | optional | Placeholder text | -| **helpText** | `string` | optional | Help/hint text | +| **label** | `string \| Record` | optional | Display label override | +| **placeholder** | `string \| Record` | optional | Placeholder text | +| **helpText** | `string \| Record` | optional | Help/hint text | | **readonly** | `boolean` | optional | Read-only override | | **immutable** | `boolean` | optional | Editable on create, locked once the record exists (e.g. machine names). | | **required** | `boolean` | optional | Required override | @@ -157,7 +157,7 @@ Column footer summary configuration | **span** | `Enum<'auto' \| 'full'>` | optional | Relative field width. 'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count (wide widgets like textarea/richtext/json/file/subform take the whole row). 'full': whole row at any column count. Prefer this over the absolute `colSpan`. | | **widget** | `string` | optional | Custom widget/component name (overrides type-based inference) | | **language** | `string` | optional | Code editor language (for type=code) | -| **keyField** | `{ field?: string; label?: string; placeholder?: string; helpText?: string; … }` | optional | Key column config for record-typed fields | +| **keyField** | `{ field?: string; label?: string \| Record; placeholder?: string \| Record; helpText?: string \| Record; … }` | optional | Key column config for record-typed fields | | **dependsOn** | `string` | optional | Parent field name for cascading | | **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — field shown only when TRUE. Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in metadata forms. No `current_user` at field level — it is unbound here and the predicate would fault open (per-option `visibleWhen` is the surface that binds it). Inside a repeater `data` is the ROW, but it is still spelled `data` — a bare identifier is unbound and faults open too. e.g. P`record.priority == 'urgent'` | | **visibleOn** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. | @@ -226,7 +226,7 @@ Column footer summary configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Stable section identifier for i18n lookup (snake_case) | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **description** | `string` | optional | Optional description rendered under the section header. | | **collapsible** | `boolean` | optional | | | **collapsed** | `boolean` | optional | | @@ -261,8 +261,8 @@ Column footer summary configuration | **drawerWidth** | `string` | optional | [DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. | | **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal forms) | | **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | -| **sections** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | | -| **groups** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | | +| **sections** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | +| **groups** | `{ name?: string; label?: string \| Record; description?: string; collapsible?: boolean; … }[]` | optional | | | **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | | **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. | | **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | @@ -424,7 +424,7 @@ List chart view configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **field** | `string` | ✅ | Field name (snake_case) | -| **label** | `string` | optional | Display label override | +| **label** | `string \| Record` | optional | Display label override | | **width** | `number` | optional | Column width in pixels | | **align** | `Enum<'left' \| 'center' \| 'right'>` | optional | Text alignment | | **hidden** | `boolean` | optional | Hide column by default | @@ -448,15 +448,15 @@ List chart view configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Internal view name (lowercase snake_case) | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | -| **columns** | `string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | | **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| … +14 more>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | | **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters | -| **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: { field: string; label?: string; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string; color?: string }[]; … }[]; tabs?: { name: string; label?: string; icon?: string; view?: string; … }[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'tabs' \| 'toggle'>; fields?: { field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string \| Record; color?: string }[]; … }[]; tabs?: { name: string; label?: string \| Record; icon?: string; view?: string; … }[]; showAllRecords?: boolean; … }` | optional | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | | **resizable** | `boolean` | optional | Enable column resizing | | **striped** | `boolean` | optional | Striped row styling | | **bordered** | `boolean` | optional | Show borders | @@ -471,7 +471,7 @@ List chart view configuration | **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | | **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **description** | `string` | optional | View description for documentation/tooltips | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | | **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: { field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[] }` | optional | Group records by one or more fields | @@ -487,12 +487,12 @@ List chart view configuration | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | | **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | | **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | -| **tabs** | `{ name: string; label?: string; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | | **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | | **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | | **allowPrinting** | `boolean` | optional | Allow users to print the view | -| **emptyState** | `{ title?: string; message?: string; icon?: string }` | optional | Empty state configuration when no records found | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | @@ -537,10 +537,10 @@ List chart view configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Internal view name (lowercase snake_case) | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | -| **columns** | `string[] \| { field: string; label?: string; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | +| **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns | | **filter** | `{ field: string; operator?: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| … +14 more>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | | **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | @@ -559,7 +559,7 @@ List chart view configuration | **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline view configuration | | **chart** | `{ chartType?: Enum<'bar' \| 'line' \| 'pie' \| 'area' \| 'scatter'>; dataset: string; dimensions?: string[]; values: string[] }` | optional | List chart view configuration | | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer } & Record` | optional | Tree/hierarchy configuration — applies when the view renders as a tree layout | -| **description** | `string` | optional | View description for documentation/tooltips | +| **description** | `string \| Record` | optional | View description for documentation/tooltips | | **sharing** | `{ type?: Enum<'personal' \| 'collaborative'>; lockedBy?: string }` | optional | View sharing and access configuration | | **rowHeight** | `Enum<'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'>` | optional | Row height / density setting | | **grouping** | `{ fields: { field: string; order?: Enum<'asc' \| 'desc'>; collapsed?: boolean }[] }` | optional | Group records by one or more fields | @@ -575,15 +575,15 @@ List chart view configuration | **exportOptions** | `Enum<'csv' \| 'xlsx' \| 'pdf' \| 'json'>[]` | optional | Available export format options | | **userActions** | `{ sort?: boolean; search?: boolean; filter?: boolean; refresh?: boolean; … }` | optional | User action toggles for the view toolbar | | **appearance** | `{ showDescription?: boolean; allowedVisualizations?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>[] }` | optional | Appearance and visualization configuration | -| **tabs** | `{ name: string; label?: string; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Tab definitions for multi-tab view interface | | **addRecord** | `{ enabled?: boolean; position?: Enum<'top' \| 'bottom' \| 'both'>; mode?: Enum<'inline' \| 'form' \| 'modal'>; formView?: string }` | optional | Add record entry point configuration | | **showRecordCount** | `boolean` | optional | Show record count at the bottom of the list | | **allowPrinting** | `boolean` | optional | Allow users to print the view | -| **emptyState** | `{ title?: string; message?: string; icon?: string }` | optional | Empty state configuration when no records found | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | +| **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string }` | optional | Empty state configuration when no records found | +| **aria** | `{ ariaLabel?: string \| Record; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | | **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | | **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | -| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: { field: string; label?: string; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string; color?: string }[]; … }[] }` | optional | | +| **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: { field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string \| Record; color?: string }[]; … }[] }` | optional | | --- @@ -595,7 +595,7 @@ List chart view configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **element** | `Enum<'dropdown' \| 'toggle'>` | ✅ | Filter control style on object views: "dropdown" (per-field value chips). "toggle" is deprecated. "tabs" is page-only — use `listViews` for named presets. | -| **fields** | `{ field: string; label?: string; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string; color?: string }[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | +| **fields** | `{ field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string \| Record; color?: string }[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | --- @@ -713,9 +713,9 @@ Quick-filter field configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **field** | `string` | ✅ | Field name on the source object (must exist — checked by reference diagnostics) | -| **label** | `string` | optional | Display label override (defaults to the field label) | +| **label** | `string \| Record` | optional | Display label override (defaults to the field label) | | **type** | `Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>` | optional | Filter control type. Omit to infer from the field definition | -| **options** | `{ value: string \| number \| boolean; label: string; color?: string }[]` | optional | Static options. Omit to derive from the field definition (select options / lookup records) | +| **options** | `{ value: string \| number \| boolean; label: string \| Record; color?: string }[]` | optional | Static options. Omit to derive from the field definition (select options / lookup records) | | **showCount** | `boolean` | optional | Show per-option record counts | | **defaultValues** | `(string \| number \| boolean)[]` | optional | Pre-selected values when the view loads | @@ -731,8 +731,8 @@ End-user quick-filter configuration (Airtable "User filters" parity) | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **element** | `Enum<'dropdown' \| 'tabs' \| 'toggle'>` | ✅ | Filter control style: "dropdown" (per-field value selectors) or "tabs" (named presets). "toggle" is deprecated. | -| **fields** | `{ field: string; label?: string; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string; color?: string }[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | -| **tabs** | `{ name: string; label?: string; icon?: string; view?: string; … }[]` | optional | Named filter presets rendered as tabs (tabs element). Reuses ViewTabSchema | +| **fields** | `{ field: string; label?: string \| Record; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string \| Record; color?: string }[]; … }[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) | +| **tabs** | `{ name: string; label?: string \| Record; icon?: string; view?: string; … }[]` | optional | Named filter presets rendered as tabs (tabs element). Reuses ViewTabSchema | | **showAllRecords** | `boolean` | optional | Show an "All records" tab before the presets (tabs element) | | **allowAddTab** | `boolean` | optional | Render an "add tab" affordance after the presets (tabs element). Page lists only — object views use `listViews` for named presets | @@ -746,11 +746,11 @@ End-user quick-filter configuration (Airtable "User filters" parity) | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. | -| **label** | `string` | optional | Human-readable label shown in metadata lists. | +| **label** | `string \| Record` | optional | Human-readable label shown in metadata lists. | | **object** | `string` | optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. | -| **list** | `{ name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | optional | | +| **list** | `{ name?: string; label?: string \| Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | optional | | | **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | | -| **listViews** | `Record; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | | **formViews** | `Record; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -869,10 +869,10 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **viewKind** | `'list'` | ✅ | | -| **config** | `{ name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | ✅ | List-family view configuration. | +| **config** | `{ name?: string; label?: string \| Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | ✅ | List-family view configuration. | | **name** | `string` | ✅ | Globally-unique view id, `.`. | | **object** | `string` | ✅ | Bound object name — the foreign key used to aggregate views. | -| **label** | `string` | optional | Display label (supports i18n). | +| **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | @@ -899,7 +899,7 @@ This schema accepts one of the following structures: | **config** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | ✅ | Form view configuration. | | **name** | `string` | ✅ | Globally-unique view id, `.`. | | **object** | `string` | ✅ | Bound object name — the foreign key used to aggregate views. | -| **label** | `string` | optional | Display label (supports i18n). | +| **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | @@ -935,10 +935,10 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **viewKind** | `'list'` | ✅ | | -| **config** | `{ name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | ✅ | List-family view configuration. | +| **config** | `{ name?: string; label?: string \| Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | ✅ | List-family view configuration. | | **name** | `string` | ✅ | Globally-unique view id, `.`. | | **object** | `string` | ✅ | Bound object name — the foreign key used to aggregate views. | -| **label** | `string` | optional | Display label (supports i18n). | +| **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | @@ -967,7 +967,7 @@ This schema accepts one of the following structures: | **config** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | ✅ | Form view configuration. | | **name** | `string` | ✅ | Globally-unique view id, `.`. | | **object** | `string` | ✅ | Bound object name — the foreign key used to aggregate views. | -| **label** | `string` | optional | Display label (supports i18n). | +| **label** | `string \| Record` | optional | Display label (supports i18n). | | **isDefault** | `boolean` | optional | Whether this is the object's default view in the switcher. | | **order** | `integer` | optional | Sort order within the object's view switcher / left rail. | | **scope** | `Enum<'package' \| 'shared' \| 'personal'>` | optional | Identity layer (defaults to `package` for source-loaded views). | @@ -1037,7 +1037,7 @@ Tab configuration for multi-tab view interface | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Tab identifier (snake_case) | -| **label** | `string` | optional | Display label | +| **label** | `string \| Record` | optional | Display label | | **icon** | `string` | optional | Tab icon name | | **view** | `string` | optional | Referenced list view name from listViews | | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| … +14 more>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Tab-specific filter criteria | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index a53a64fed3..d887a9a5ad 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -273,4 +273,4 @@ directory rather than per file. | `kernel/` | 319 | | `qa/` | 6 | | `shared/` | 20 | -| `system/` | 367 | +| `system/` | 368 | diff --git a/examples/app-showcase/src/system/translations/index.ts b/examples/app-showcase/src/system/translations/index.ts index 4bc38c1655..fac1cd9cc8 100644 --- a/examples/app-showcase/src/system/translations/index.ts +++ b/examples/app-showcase/src/system/translations/index.ts @@ -278,6 +278,21 @@ export const ShowcaseTranslationBundle = { split_task: { label: '任务' }, split_schedule: { label: '排期' }, }, + // The filter-preset tab bar of `ui/pages/task-triage.page.ts` — four + // tabs carrying a `filter` and no `view`, which is the shape that had + // no translation slot at all until `_tabs` landed (#5377). Born + // translated rather than ratcheted into the baseline as new debt. + // + // Wording is the vocabulary this bundle already uses for the same + // ideas: each tab filters on a `status` / `priority` value whose option + // label is right above, so the tab reads the same word as the cell it + // filters by. + _tabs: { + in_progress: { label: '进行中' }, + urgent: { label: '紧急' }, + in_review: { label: '评审中' }, + done: { label: '已完成' }, + }, }, showcase_account: { label: '客户', diff --git a/packages/cli/src/utils/i18n-extract.ts b/packages/cli/src/utils/i18n-extract.ts index 855f8ed6d9..36e7dff897 100644 --- a/packages/cli/src/utils/i18n-extract.ts +++ b/packages/cli/src/utils/i18n-extract.ts @@ -539,6 +539,76 @@ function walkObjectSections(config: any, out: ExpectedEntry[]): void { } } +// ─── Object filter-preset tabs (`objects.._tabs..label`) ─────── +// +// The writing half of the `_tabs` slot `ObjectTranslationDataSchema` declares +// and `resolveTabLabel` reads (#5377). Declared, written and read in one PR, +// which is the whole point: a slot with no extractor is a key a translator has +// to know exists, and that is most of why the tab bar stayed English. +// +// Scope matches the resolver exactly — a list page's +// `interfaceConfig.userFilters.tabs`, the one `ViewTabSchema` carrier anything +// renders. `ListViewSchema.tabs` has no reader in either repo; scaffolding keys +// for it would fill every bundle with entries no screen can ever show, and the +// coverage gate would then demand translations for them. +// +// A tab that references a saved view still gets its own entry. The resolver +// falls back to the referenced view's label when `_tabs` is empty, so the key +// is genuinely optional — but it is the only way to give a tab a label that +// DIFFERS from the view it opens, and a skeleton that omitted it would hide +// that from the translator. + +/** Emit `objects.._tabs..label` for every rendered preset tab. */ +function walkObjectTabs(config: any, out: ExpectedEntry[]): void { + const pages: any[] = Array.isArray(config?.pages) ? config.pages : []; + // One tab name may be authored on several pages over the same object (a + // shared "urgent" preset); collect first so the key is emitted once, the + // same way `walkObjectSections` de-duplicates a heading. + const index = new Map>(); + + for (const page of pages) { + if (!page || typeof page !== 'object') continue; + const cfg = page.interfaceConfig; + const tabs = cfg?.userFilters?.tabs; + if (!Array.isArray(tabs)) continue; + // The page's own source binding first, then its record binding — the order + // `translatePage` resolves the object in. + const objectName = inlineText(cfg?.source) ?? inlineText(page.object); + if (objectName === undefined) continue; + + let tabsForObject = index.get(objectName); + if (!tabsForObject) index.set(objectName, (tabsForObject = new Map())); + + for (const tab of tabs) { + if (!tab || typeof tab !== 'object') continue; + const tabName = inlineText(tab.name); + if (tabName === undefined) continue; + // A localized-map label is already multilingual — `inlineText` drops it, + // so the entry is emitted with no seed and coverage does not demand a + // translation for a string nobody authored in plain text. + const authored = inlineText(tab.label); + if (!tabsForObject.has(tabName)) tabsForObject.set(tabName, authored); + else if (tabsForObject.get(tabName) === undefined && authored !== undefined) { + tabsForObject.set(tabName, authored); + } + } + } + + for (const [objectName, tabs] of index) { + for (const [tabName, label] of tabs) { + pushDerived( + out, + ['objects', objectName, '_tabs', tabName, 'label'], + // Seed mirrors the renderer's own fallback (`tab.label || tab.name`). + label ?? tabName, + label, + 'view', + { objectName }, + ); + } + } +} + /** Collect every translatable entry from a normalized stack config. */ export function collectExpectedEntries(config: any): ExpectedEntry[] { const out: ExpectedEntry[] = []; @@ -790,6 +860,9 @@ export function collectExpectedEntries(config: any): ExpectedEntry[] { // from being counted twice against coverage. walkObjectSections(config, out); + // ── Object filter-preset tabs (`objects.._tabs..label`) ─── + walkObjectTabs(config, out); + // ── Metadata configuration forms (Studio admin UI) ──────────────── // Registry-driven: always included, independent of stack config. These // emit under `metadataForms..*` so the generic renderer can pick diff --git a/packages/cli/test/i18n-tab-coverage.test.ts b/packages/cli/test/i18n-tab-coverage.test.ts new file mode 100644 index 0000000000..418bbec941 --- /dev/null +++ b/packages/cli/test/i18n-tab-coverage.test.ts @@ -0,0 +1,208 @@ +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. +// +// objectstack#5377 — a list page's filter-preset TAB labels were the surface +// with no key at all. `ViewTabSchema.label` is an `I18nLabelSchema`, +// `ObjectTranslationDataSchema` had no `_tabs` member and (being strict) +// rejected the key a translator invented, and no resolver read one. So the tab +// bar rendered in the source language above a fully localized grid, with no +// authoring workaround anywhere. +// +// ## What the fix is NOT +// +// The issue's headline — an entire tab bar in English — was retracted by the +// reporter on rc.2 (comment 2026-08-05): a tab that carries `view` already +// renders its referenced view's translated label, because the console follows +// the reference into `_views..label`. That path is preserved here, as the +// SECOND step of `resolveTabLabel`'s chain, and the tests below pin it. +// +// What genuinely had nothing was the **filter-only tab** — `{ name, label, +// filter }` with no `view`. It references nothing to inherit from. That shape +// is legal under `ViewTabSchema` and it is what the showcase authors +// (`task-triage.page.ts`: In Progress / Urgent / In Review / Done). +// +// ## Why `objects.._tabs` and not `pages.

.tabs` +// +// A tab is a named filter preset over ONE object's records — "Urgent" names a +// slice of tasks. That is object vocabulary, the same way a saved view's label +// is, and it is what makes the view-reference fallback expressible: both halves +// of the chain live in one object's namespace. `_sections` is the shape +// precedent — those are authored on page components too and are addressed under +// their object. + +import { describe, it, expect } from 'vitest'; +import { collectExpectedEntries, extractTranslations } from '../src/utils/i18n-extract'; +import { computeI18nCoverage } from '../src/utils/i18n-coverage'; +import { ObjectTranslationDataSchema } from '@objectstack/spec/system'; + +/** Every `objects.._tabs.*` path the walker emits, as dot-paths. */ +const tabKeys = (config: any) => + collectExpectedEntries(config) + .filter((e) => e.path[2] === '_tabs') + .map((e) => e.path.join('.')); + +const tabEntries = (config: any) => + collectExpectedEntries(config).filter((e) => e.path[2] === '_tabs'); + +/** A list page whose preset bar is filter-only — the #5377 shape. */ +const triagePage = (overrides: Record = {}) => ({ + name: 'showcase_task_triage', + label: 'Task Triage', + type: 'list', + object: 'showcase_task', + interfaceConfig: { + source: 'showcase_task', + userFilters: { + element: 'tabs', + showAllRecords: true, + tabs: [ + { name: 'in_progress', label: 'In Progress', filter: [{ field: 'status', operator: 'equals', value: 'in_progress' }] }, + { name: 'urgent', label: 'Urgent', icon: 'flame', filter: [{ field: 'priority', operator: 'equals', value: 'urgent' }] }, + ], + }, + }, + ...overrides, +}); + +describe('the extractor scaffolds a key for every rendered preset tab', () => { + it('emits `objects.._tabs..label`', () => { + expect(tabKeys({ pages: [triagePage()] }).sort()).toEqual([ + 'objects.showcase_task._tabs.in_progress.label', + 'objects.showcase_task._tabs.urgent.label', + ]); + }); + + it('seeds the entry with the authored literal, so the skeleton is usable', () => { + const entries = tabEntries({ pages: [triagePage()] }); + const urgent = entries.find((e) => e.path[3] === 'urgent'); + expect(urgent?.sourceValue).toBe('Urgent'); + expect(urgent?.inline).toBe('Urgent'); + expect(urgent?.objectName).toBe('showcase_task'); + }); + + it('falls back to the tab name as the seed, and leaves `inline` unset', () => { + // A tab with no authored label renders its name. Scaffolding the name keeps + // the skeleton usable, but coverage must not demand a translation of a + // string nobody wrote — the same `pushDerived` contract sections use. + const page = triagePage(); + delete (page.interfaceConfig.userFilters.tabs[1] as Record).label; + const urgent = tabEntries({ pages: [page] }).find((e) => e.path[3] === 'urgent'); + expect(urgent?.sourceValue).toBe('urgent'); + expect(urgent?.inline).toBeUndefined(); + }); + + it('treats an inline locale map as already multilingual (#5728)', () => { + // The other authorized label form. It is not plain source text, so there is + // nothing to scaffold and nothing to demand. + const page = triagePage(); + (page.interfaceConfig.userFilters.tabs[1] as Record).label = + { en: 'Urgent', 'zh-CN': '紧急' }; + const urgent = tabEntries({ pages: [page] }).find((e) => e.path[3] === 'urgent'); + expect(urgent?.inline).toBeUndefined(); + }); + + it('keys on `interfaceConfig.source` before the page-level `object`', () => { + // A list page retargeted at another object filters THAT object's records. + const keys = tabKeys({ pages: [triagePage({ object: 'crm_lead' })] }); + expect(keys.every((k) => k.startsWith('objects.showcase_task.'))).toBe(true); + }); + + it('falls back to the page `object` when the config declares no source', () => { + const page = triagePage(); + delete (page.interfaceConfig as Record).source; + expect(tabKeys({ pages: [page] })).toContain('objects.showcase_task._tabs.urgent.label'); + }); + + it('emits one key per (object, tab) however many pages declare it', () => { + // Two pages over the same object sharing an "urgent" preset is one string, + // not two — counting it twice would inflate every coverage report. + const second = triagePage({ name: 'showcase_task_board' }); + expect(tabKeys({ pages: [triagePage(), second] }).sort()).toEqual([ + 'objects.showcase_task._tabs.in_progress.label', + 'objects.showcase_task._tabs.urgent.label', + ]); + }); + + it('emits nothing for a page with no preset bar', () => { + expect(tabKeys({ pages: [{ name: 'p', object: 'showcase_task', regions: [] }] })).toEqual([]); + }); + + it('does not scaffold the carrier nothing renders (`ListView.tabs`)', () => { + // `ViewTabSchema` has two carriers. Only `userFilters.tabs` on a page has a + // renderer (objectui's `TabFilters`); nothing in either repo reads a list + // view's own `tabs`. Scaffolding it would fill every bundle with keys no + // screen can show — and then the coverage gate would demand translations + // for them. + const viewWithTabs = { + name: 'showcase_task', + object: 'showcase_task', + listViews: { + all: { type: 'grid', columns: ['title'], tabs: [{ name: 'urgent', label: 'Urgent' }] }, + }, + }; + expect(tabKeys({ views: [viewWithTabs] })).toEqual([]); + }); +}); + +describe('the emitted key is the key the schema declares', () => { + it('`ObjectTranslationDataSchema` accepts a bundle written at the extracted path', () => { + // Extractor and schema agreeing is the whole contract: a key `os i18n + // extract` writes that `.strict()` then rejects is worse than no key. + const keys = tabKeys({ pages: [triagePage()] }); + expect(keys.length).toBeGreaterThan(0); + + const data: Record = {}; + for (const key of keys) data[key.split('.')[3]] = { label: '译' }; + + expect(() => ObjectTranslationDataSchema.parse({ _tabs: data })).not.toThrow(); + }); + + it('a hand-invented spelling is still rejected — the slot did not go open', () => { + expect(() => ObjectTranslationDataSchema.parse({ + _tabs: { urgent: { label: '紧急', tooltip: 'x' } }, + })).toThrow(); + }); +}); + +describe('coverage', () => { + const config = () => ({ + pages: [triagePage()], + i18n: { supportedLocales: ['en', 'zh-CN'] }, + translations: [ + { 'zh-CN': { objects: { showcase_task: { _tabs: { in_progress: { label: '进行中' } } } } } }, + ], + }); + + const tabIssuePaths = (report: { issues: Array<{ key: string }> }) => + report.issues.filter((i) => i.key.includes('._tabs.')).map((i) => i.key); + + it('reports the untranslated tab and stays quiet about the translated one', () => { + expect(tabIssuePaths(computeI18nCoverage(config()))) + .toEqual(['objects.showcase_task._tabs.urgent.label']); + }); + + it('a project that declares no locales still reports nothing', () => { + // The opt-in contract every surface in this walker has to keep: a + // monolingual project must not start failing lint for a feature it never + // asked for. + expect(tabIssuePaths(computeI18nCoverage({ pages: [triagePage()] }))).toEqual([]); + }); + + it('`extractTranslations` writes the tab keys into the skeleton', () => { + const out = extractTranslations({ pages: [triagePage()] }, { locales: ['zh-CN'] }); + const zh = out.bundles['zh-CN'] as any; + expect(zh?.objects?.showcase_task?._tabs?.urgent).toBeDefined(); + }); +}); + +describe('the showcase app, walked for real', () => { + it('picks up `showcase_task_triage`’s four filter-only presets', async () => { + const { TaskTriagePage } = await import('../../../examples/app-showcase/src/ui/pages/task-triage.page'); + + expect(tabKeys({ pages: [TaskTriagePage] }).sort()).toEqual([ + 'objects.showcase_task._tabs.done.label', + 'objects.showcase_task._tabs.in_progress.label', + 'objects.showcase_task._tabs.in_review.label', + 'objects.showcase_task._tabs.urgent.label', + ]); + }, 60_000); +}); diff --git a/packages/spec/api-surface/system.json b/packages/spec/api-surface/system.json index 9f9a215fdd..d9dcd39723 100644 --- a/packages/spec/api-surface/system.json +++ b/packages/spec/api-surface/system.json @@ -747,6 +747,7 @@ "VectorClock (type)", "VectorClockSchema (const)", "ViewLike (interface)", + "ViewTabLike (interface)", "WidgetLike (interface)", "WorkerConfig (type)", "WorkerConfigParsed (type)", @@ -800,6 +801,7 @@ "resolveSettingsOptionLabel (function)", "resolveSettingsSourceLabel (function)", "resolveSettingsTitle (function)", + "resolveTabLabel (function)", "resolveViewDescription (function)", "resolveViewLabel (function)", "s3StorageExample (const)", diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 73c8edd458..c7dd6566e4 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -175,6 +175,8 @@ "InlineAction (type)", "InlineActionParsed (type)", "InlineActionSchema (const)", + "InlineLocaleMap (type)", + "InlineLocaleMapSchema (const)", "InterfacePageConfig (type)", "InterfacePageConfigParsed (type)", "InterfacePageConfigSchema (const)", diff --git a/packages/spec/authorable-surface/system.json b/packages/spec/authorable-surface/system.json index c9da8ba25d..da9c46e05b 100644 --- a/packages/spec/authorable-surface/system.json +++ b/packages/spec/authorable-surface/system.json @@ -885,6 +885,7 @@ "system/ObjectStorageConfig:scope", "system/ObjectTranslationData:_actions", "system/ObjectTranslationData:_sections", + "system/ObjectTranslationData:_tabs", "system/ObjectTranslationData:_views", "system/ObjectTranslationData:description", "system/ObjectTranslationData:fields", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index 52193becd1..9f157a92d9 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -76,6 +76,7 @@ "ui/HttpRequest", "ui/I18nLabel", "ui/InlineAction", + "ui/InlineLocaleMap", "ui/InterfacePageConfig", "ui/JoinedReportBlock", "ui/KanbanConfig", diff --git a/packages/spec/src/system/i18n-resolver.test.ts b/packages/spec/src/system/i18n-resolver.test.ts index 2c394515e8..25d406b2b9 100644 --- a/packages/spec/src/system/i18n-resolver.test.ts +++ b/packages/spec/src/system/i18n-resolver.test.ts @@ -1132,6 +1132,192 @@ describe('translatePage', () => { }); }); +// ──────────────────────────────────────────────────────────────────────────── +// #5377 — filter-preset tab labels (`objects.._tabs..label`) +// ──────────────────────────────────────────────────────────────────────────── + +import { resolveTabLabel } from './i18n-resolver'; + +describe('resolveTabLabel (#5377)', () => { + const bundle: TranslationBundle = { + 'zh-CN': { + objects: { + showcase_task: { + _tabs: { urgent: { label: '紧急' } }, + _views: { my_open: { label: '我的待办' } }, + }, + }, + }, + en: { + objects: { + showcase_task: { + _tabs: { done: { label: 'Completed' } }, + _views: { my_open: { label: 'My Open Tasks' } }, + }, + }, + }, + }; + + // ── the gap this closes: a tab with a `filter` and NO `view` ───────────── + // + // The reported headline (a whole tab bar in English) did not reproduce on + // rc.2 and is not what this fixes: a tab that carries `view` already renders + // its referenced view's translated label. What had no path at all was the + // filter-only tab — it references nothing to inherit from, `_tabs` did not + // exist, and `ObjectTranslationDataSchema` is strict, so a translator who + // invented the key had it rejected rather than ignored. + + it('translates a filter-only tab — the shape that had no path at all', () => { + const tab = { name: 'urgent', label: 'Urgent', filter: [{ field: 'priority', operator: 'equals', value: 'urgent' }] }; + expect(resolveTabLabel(bundle, 'showcase_task', tab, { locale: 'zh-CN' })).toBe('紧急'); + }); + + it('falls back to the literal label when the bundle has no entry', () => { + const tab = { name: 'in_review', label: 'In Review' }; + expect(resolveTabLabel(bundle, 'showcase_task', tab, { locale: 'zh-CN' })).toBe('In Review'); + }); + + it('falls back to the tab name when there is no literal label either', () => { + expect(resolveTabLabel(bundle, 'showcase_task', { name: 'in_review' }, { locale: 'zh-CN' })).toBe('in_review'); + }); + + // ── the pre-existing path is PRESERVED, not replaced ───────────────────── + + it('reads the referenced view label for a `view` tab with no `_tabs` entry', () => { + const tab = { name: 'mine', label: 'Mine', view: 'my_open' }; + expect(resolveTabLabel(bundle, 'showcase_task', tab, { locale: 'zh-CN' })).toBe('我的待办'); + }); + + it('an explicit `_tabs` entry outranks the referenced view label', () => { + // The tab bar may deliberately name the same view differently ("Urgent" + // over a view called "High-priority open tasks"); the specific key wins. + const tab = { name: 'urgent', label: 'Urgent', view: 'my_open' }; + expect(resolveTabLabel(bundle, 'showcase_task', tab, { locale: 'zh-CN' })).toBe('紧急'); + }); + + it('an explicit `_tabs` entry in a FALLBACK locale still outranks the view label', () => { + // Both steps run the whole chain before the next one starts, so a `_tabs` + // entry that only exists in `en` beats a `zh-CN` view label. Precedence is + // by specificity of the key, not by position in the locale chain. + const tab = { name: 'done', label: 'Done', view: 'my_open' }; + expect(resolveTabLabel(bundle, 'showcase_task', tab, { locale: 'zh-CN', fallbackChain: ['en'] })) + .toBe('Completed'); + }); + + it('degrades to the literal when there is no bundle or no object binding', () => { + const tab = { name: 'urgent', label: 'Urgent' }; + expect(resolveTabLabel(undefined, 'showcase_task', tab, { locale: 'zh-CN' })).toBe('Urgent'); + expect(resolveTabLabel(bundle, undefined, tab, { locale: 'zh-CN' })).toBe('Urgent'); + }); +}); + +describe('translatePage — filter-preset tab bar (#5377)', () => { + const bundle: TranslationBundle = { + 'zh-CN': { + objects: { + showcase_task: { _tabs: { in_progress: { label: '进行中' }, urgent: { label: '紧急' } } }, + }, + }, + }; + + const triagePage = () => ({ + name: 'showcase_task_triage', + label: 'Task Triage', + type: 'list', + object: 'showcase_task', + interfaceConfig: { + source: 'showcase_task', + userFilters: { + element: 'tabs', + showAllRecords: true, + tabs: [ + { name: 'in_progress', label: 'In Progress' }, + { name: 'urgent', label: 'Urgent', icon: 'flame' }, + { name: 'done', label: 'Done' }, + ], + }, + }, + }); + + const tabs = (doc: any) => doc.interfaceConfig.userFilters.tabs; + + it('translates the tabs the bundle covers and leaves the rest literal', () => { + const out = translatePage(triagePage(), bundle, { locale: 'zh-CN' }); + expect(tabs(out).map((t: any) => t.label)).toEqual(['进行中', '紧急', 'Done']); + }); + + it('keeps every other key on the tab intact', () => { + const out = translatePage(triagePage(), bundle, { locale: 'zh-CN' }); + expect(tabs(out)[1].icon).toBe('flame'); + expect(out.interfaceConfig.userFilters.element).toBe('tabs'); + expect(out.interfaceConfig.userFilters.showAllRecords).toBe(true); + }); + + it('resolves the object from `interfaceConfig.source` before the page `object`', () => { + // A list page retargeted at another object keys its presets under the + // object whose records they filter — the same "component's own binding + // first" rule `_sections` follows. With the page-level binding alone this + // would look up `crm_lead._tabs` and find nothing. + const retargeted = { ...triagePage(), object: 'crm_lead' }; + const out = translatePage(retargeted, bundle, { locale: 'zh-CN' }); + expect(tabs(out)[0].label).toBe('进行中'); + }); + + it('falls back to the page `object` when `source` is absent', () => { + const page = triagePage(); + delete (page.interfaceConfig as Record).source; + const out = translatePage(page, bundle, { locale: 'zh-CN' }); + expect(tabs(out)[0].label).toBe('进行中'); + }); + + it('leaves an inline-locale-map label alone rather than flattening it (#5728)', () => { + // Since the union widening a tab label may itself be multilingual. That is + // the author's own resolution route (`pickLocalized` picks at render time); + // overwriting it with a bundle miss would turn four languages into one. + const page = triagePage(); + (page.interfaceConfig.userFilters.tabs as any[])[2].label = { en: 'Done', 'ja-JP': '完了' }; + const out = translatePage(page, bundle, { locale: 'ja-JP' }); + expect(tabs(out)[2].label).toEqual({ en: 'Done', 'ja-JP': '完了' }); + }); + + it('leaves a page with no tab bar untouched', () => { + const plain = { name: 'p', label: 'P', regions: [] }; + expect(translatePage(plain, bundle, { locale: 'zh-CN' })).not.toHaveProperty('interfaceConfig'); + }); + + it('does not mutate the input page', () => { + const doc = triagePage(); + const snapshot = JSON.parse(JSON.stringify(doc)); + translatePage(doc, bundle, { locale: 'zh-CN' }); + expect(doc).toEqual(snapshot); + }); +}); + +describe('ObjectTranslationDataSchema._tabs (#5377)', () => { + it('accepts a `_tabs` entry — the key a strict schema used to reject', () => { + const data = TranslationDataSchema.parse({ + objects: { showcase_task: { _tabs: { urgent: { label: '紧急' } } } }, + }); + expect(data.objects?.showcase_task?._tabs?.urgent?.label).toBe('紧急'); + }); + + it('renames the spellings a translator reaches for first', () => { + const r = TranslationDataSchema.safeParse({ + objects: { showcase_task: { _tabs: { urgent: { title: '紧急' } } } }, + }); + expect(r.success).toBe(false); + expect(JSON.stringify(r.error?.issues)).toContain('`title` → `label`'); + }); + + it('still rejects an undeclared key and names the surface', () => { + const r = TranslationDataSchema.safeParse({ + objects: { showcase_task: { _tabs: { urgent: { label: '紧急', tooltip: 'x' } } } }, + }); + expect(r.success).toBe(false); + expect(JSON.stringify(r.error?.issues)).toContain('this view tab translation'); + }); +}); + describe('TranslationDataSchema pages', () => { it('accepts a fully-populated pages entry', () => { const data = TranslationDataSchema.parse({ diff --git a/packages/spec/src/system/i18n-resolver.ts b/packages/spec/src/system/i18n-resolver.ts index dd1899842d..773b66854e 100644 --- a/packages/spec/src/system/i18n-resolver.ts +++ b/packages/spec/src/system/i18n-resolver.ts @@ -14,6 +14,7 @@ * objects.._actions..label * objects.._actions..confirmText * objects.._actions..successMessage + * objects.._tabs..label * * `` is the BARE authoring key (`listViews.`, or the default * list/form key) — never the `.` identity the registry assigns a @@ -257,6 +258,87 @@ export function resolveViewDescription( return view.description; } +/** + * Minimal filter-preset tab shape consumed by {@link resolveTabLabel} — + * `ViewTabSchema` (`ui/view.zod.ts`) narrowed to what the lookup reads. + */ +export interface ViewTabLike { + /** Tab identifier (snake_case) — the `_tabs` key. */ + name: string; + label?: string; + /** Referenced list view name, when the tab is a saved-view shortcut. */ + view?: string; + [key: string]: unknown; +} + +/** + * Resolve a translated filter-preset tab label (#5377). + * + * Lookup order, per locale in the chain: + * + * 1. `objects.._tabs..label` — the explicit translation. + * 2. `objects.._views..label` — for a tab that references a + * saved view, the view's own translated label. This is the path that + * already worked before `_tabs` existed (the console follows `tabs[].view` + * and reads the referenced view's label), so it is preserved rather than + * replaced: an app that translated its views and never wrote a `_tabs` + * entry keeps rendering exactly what it rendered before. + * 3. The literal `tab.label`, then `tab.name`. + * + * A `filter`-only tab — legal under `ViewTabSchema`, and the shape the showcase + * authors — has no step 2, which is precisely why it had no translation path at + * all before this. Each step is evaluated across the whole locale chain in + * order, so an explicit `_tabs` entry in ANY locale of the chain outranks a + * view label: the more specific key wins over the inherited one, the same + * precedence `translatePage` gives component-id copy over page-name copy. + */ +export function resolveTabLabel( + bundle: TranslationBundle | undefined, + objectName: string | undefined, + tab: ViewTabLike, + opts?: ResolveOptions, +): string { + return lookupTabLabel(bundle, objectName, tab, opts) + ?? (typeof tab.label === 'string' ? tab.label : tab.name); +} + +/** + * The bundle-only half of {@link resolveTabLabel}: `undefined` when neither + * `_tabs` nor the referenced view's label carries a translation. + * + * `translatePage` needs this distinction rather than the resolved string. + * Since #5728 a `label` may itself be an inline locale map, which is already + * multilingual and is the author's own resolution route — overwriting it with + * `resolveTabLabel`'s string fallback would flatten a four-language label down + * to one. Writing only when the bundle actually answered leaves that shape + * untouched. + */ +function lookupTabLabel( + bundle: TranslationBundle | undefined, + objectName: string | undefined, + tab: ViewTabLike, + opts?: ResolveOptions, +): string | undefined { + if (!bundle || !objectName || typeof tab.name !== 'string' || tab.name.length === 0) { + return undefined; + } + const chain = localeChain(opts); + + for (const code of chain) { + const candidate = pickData(bundle, code)?.objects?.[objectName]?._tabs?.[tab.name]?.label; + if (typeof candidate === 'string' && candidate.length > 0) return candidate; + } + + if (typeof tab.view === 'string' && tab.view.length > 0) { + for (const code of chain) { + const candidate = pickData(bundle, code)?.objects?.[objectName]?._views?.[tab.view]?.label; + if (typeof candidate === 'string' && candidate.length > 0) return candidate; + } + } + + return undefined; +} + function lookupActionField( bundle: TranslationBundle | undefined, action: ActionLike, @@ -697,6 +779,18 @@ export interface PageLike { label?: string; description?: string; regions?: PageRegionLike[]; + /** Bound object for a record page — the `_tabs` fallback binding (#5377). */ + object?: string; + /** + * List/interface page config (`InterfacePageConfigSchema`). Carries the + * `userFilters.tabs` preset bar this resolver translates, and the `source` + * object those presets filter. + */ + interfaceConfig?: { + source?: string; + userFilters?: { tabs?: ViewTabLike[]; [key: string]: unknown }; + [key: string]: unknown; + }; [key: string]: any; } @@ -787,6 +881,12 @@ function lookupPageComponentCopy( * Only region-level components are visited: `page:header` is a top-level * layout block by convention, and components nested inside another component's * `properties` (tabs, sections) are untyped free-form props. + * + * A list page's filter-preset tab bar + * (`interfaceConfig.userFilters.tabs[].label`) is translated too, against + * `objects.._tabs..label` — see + * {@link translateInterfaceTabs} for why that key is object-scoped and why + * only this one of `ViewTabSchema`'s two carriers is covered (#5377). */ export function translatePage( doc: T, @@ -855,14 +955,64 @@ export function translatePage( }) : doc.regions; + const interfaceConfig = translateInterfaceTabs(doc, bundle, opts); + return { ...doc, ...(label !== undefined ? { label } : {}), ...(description !== undefined ? { description } : {}), ...(regions !== undefined ? { regions } : {}), + ...(interfaceConfig !== undefined ? { interfaceConfig } : {}), }; } +/** + * Translate a list page's `interfaceConfig.userFilters.tabs[].label` against + * `objects.._tabs..label` (#5377). Returns `undefined` when + * the page has no tab bar or nothing resolved, so `translatePage` leaves the + * key off the copy entirely. + * + * **This is where `ViewTabSchema` is actually rendered.** The schema has two + * carriers — `UserFiltersSchema.tabs` (page-only preset bar, ADR-0047) and + * `ListViewSchema.tabs` ("multi-tab view interface") — and only the first has a + * renderer: objectui's `TabFilters` draws it from a page's `interfaceConfig`, + * while nothing in either repo reads the ListView carrier. Translating the + * carrier nothing draws would declare a capability no user can see, so this + * covers the live one and stops there. + * + * The object comes from `interfaceConfig.source` — the page's own binding for + * the records these presets filter — falling back to the page-level `object`. + * Same order `i18n-extract` uses when it scaffolds the keys, and the same + * "component's own binding first" discipline `_sections` follows. + */ +function translateInterfaceTabs( + doc: PageLike, + bundle: TranslationBundle | undefined, + opts?: ResolveOptions, +): PageLike['interfaceConfig'] | undefined { + const cfg = doc.interfaceConfig; + if (!cfg || typeof cfg !== 'object') return undefined; + const userFilters = cfg.userFilters; + if (!userFilters || typeof userFilters !== 'object' || !Array.isArray(userFilters.tabs)) { + return undefined; + } + const objectName = typeof cfg.source === 'string' && cfg.source.length > 0 + ? cfg.source + : doc.object; + if (!objectName) return undefined; + + let changed = false; + const tabs = userFilters.tabs.map((tab) => { + if (!tab || typeof tab !== 'object') return tab; + const label = lookupTabLabel(bundle, objectName, tab, opts); + if (label === undefined) return tab; + changed = true; + return { ...tab, label }; + }); + + return changed ? { ...cfg, userFilters: { ...userFilters, tabs } } : undefined; +} + // ──────────────────────────────────────────────────────────────────────────── // Object metadata resolvers (label / pluralLabel / description / fields / options) // ──────────────────────────────────────────────────────────────────────────── diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index 04f144fe52..446194868f 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -221,6 +221,35 @@ export const ObjectTranslationDataSchema = lazySchema(() => strictObject({ label: z.string().optional().describe('Translated section label'), description: z.string().optional().describe('Translated section description'), })).optional().describe('Section translations keyed by section name'), + + /** + * Filter-preset tab translations keyed by tab name (`ViewTabSchema.name`). + * Convention (auto-resolved by `resolveTabLabel`): + * objects.._tabs..label + * + * **The hole this closes (#5377).** A tab that references a saved view + * (`tabs[].view`) already renders a translated label — the console follows + * the reference and reads `_views..label`. A tab that carries only a + * `filter` references nothing, and there was no key to put its label in: + * this shape is a `strictObject`, so a translator who invented `_tabs` had it + * rejected rather than ignored. The tab bar then sat above a fully localized + * grid in the source language, with no authoring workaround. + * + * **Why it is object-scoped, next to `_views`, rather than under `pages`.** + * A tab is a named filter preset over one object's records — "Urgent" names a + * slice of tasks, which is object vocabulary in the same way a saved view's + * label is. It is also what makes the view-reference fallback expressible at + * all: both halves of `resolveTabLabel`'s chain live in one object's + * namespace. `_sections` is the shape precedent — those live on page + * components too and are addressed under their object, not their page. + */ + _tabs: z.record(z.string(), strictObject({ + surface: 'this view tab translation', + history: TRANSLATION_HISTORY, + aliases: { name: 'label', title: 'label', heading: 'label', text: 'label' }, + }, { + label: z.string().optional().describe('Translated tab label'), + })).optional().describe('Filter-preset tab translations keyed by tab name'), }).describe('Translation data for a single object')); export type ObjectTranslationData = z.input; @@ -476,9 +505,7 @@ const translationDataShape = () => ({ * * **The key face is measured, not mirrored.** Each key below is a copy prop * that some component in `ComponentPropsMap` (`ui/component.zod.ts`) - * actually declares — every one is a plain `z.string()`/`I18nLabelSchema`, - * i.e. a literal with no inline `{en, zh}` form, so the bundle is its ONLY - * localization route: + * actually declares: * * | key | declared by | * |:---|:---| @@ -504,6 +531,17 @@ const translationDataShape = () => ({ * `properties` is an open record and custom component types are legal, so * these keys are also the route for a bespoke component that speaks the * same vocabulary (hotcrm's `ai_briefing` carries `title` + `description`). + * + * ⚠️ The bundle is no longer these keys' ONLY localization route (#5728). + * `I18nLabelSchema` — which most of them are declared as — now also accepts + * an inline `{ en, 'zh-CN' }` locale map, so a component's copy may already + * be multilingual at the authoring site. That does not narrow this face: + * `translatePage` writes only where the bundle actually has an entry, so an + * inline map the bundle does not cover is left intact rather than flattened + * to one language, and a bundle entry still wins where both exist. The + * bundle stays the route that scales (translators never open a + * `*.page.ts`); the inline map is the route for copy that ships with the + * page. */ components: z.record(z.string(), strictObject({ surface: 'this page component translation', diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 518a2d16ad..e387580d54 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -139,15 +139,24 @@ import { FeedItemType, FeedFilterMode } from '../data/feed.zod'; // `saveMetaItem` / REST `/meta` write still stores an unvalidated props bag // (#4463's fourth wall). That is recorded, not fixed, by #5068. // -// The gate is WARNING-level in this first step. The live corpus violates these -// declarations in places that are open contract questions rather than authoring -// mistakes — inline `{ en, 'zh-CN' }` label maps on three published platform -// pages against an `I18nLabelSchema` that is a plain `z.string()` (#5728), and -// keys objectui's renderers honour that this file does not declare. The -// warning-period inventory is the acceptance baseline for the error upgrade. -// #5775 cleared this file's half of that inventory; #5728 (the label maps) and -// the page rewrites (`page:card.visible`, #5776's tab `key`) are what remain -// before the upgrade to error. +// The gate is WARNING-level in this first step. The live corpus violated these +// declarations in places that were open contract questions rather than +// authoring mistakes, and the inventory is the acceptance baseline for the +// error upgrade. Two of the three entries are now cleared: +// +// - #5775 declared the keys objectui's renderers honour and tombstoned the +// four nothing read. +// - #5728 settled the inline `{ en, 'zh-CN' }` label maps the three published +// platform pages author: the maintainer ruled (2026-08-06) that the map is a +// delivered capability, so `I18nLabelSchema` is a union of the plain string +// and an inline locale map, and `element:text.content` — declared a bare +// `z.string()` and therefore out of that union's reach — was named in the +// same ruling and moved onto it. That retired all 42 `component-props-invalid` +// findings this gate reported on the platform pages (34 label + 8 content). +// +// What remains before the upgrade to error is the page rewrites +// (`page:card.visible` → the component-level `visibleWhen`, #5776's tab `key` +// → `value`), not a declaration in this file. // // The verdict is pinned in `component.test.ts` and in the `ui/` tables of // `docs/audits/2026-07-unknown-key-strictness-ledger.md` — change all three @@ -509,7 +518,19 @@ export const AIChatWindowProps = z.object({ */ export const ElementTextPropsSchema = lazySchema(() => z.object({ - content: z.string().describe('Text or Markdown content'), + /** + * Text or Markdown body copy. + * + * `I18nLabelSchema` rather than a bare `z.string()` (#5728, named explicitly + * in the maintainer's ruling because the label-wide widening could not reach + * it): `sys-user.page.ts` authors eight `element:text` nodes whose `content` + * is an inline `{ en, 'zh-CN', 'ja-JP', 'es-ES' }` map, and objectui resolves + * them through the same `pickLocalized` every label goes through. The bare + * string was the declaration disagreeing with the delivered shape, and it was + * eight of the 42 findings the #5068 gate reported on the platform's own + * pages. + */ + content: I18nLabelSchema.describe('Text or Markdown content — a plain string, or an inline locale map'), variant: z.enum(['heading', 'subheading', 'body', 'caption']) .optional().default('body').describe('Text style variant'), align: z.enum(['left', 'center', 'right']) diff --git a/packages/spec/src/ui/i18n.test.ts b/packages/spec/src/ui/i18n.test.ts index e6a3136093..039ecc462b 100644 --- a/packages/spec/src/ui/i18n.test.ts +++ b/packages/spec/src/ui/i18n.test.ts @@ -21,14 +21,53 @@ describe('I18nLabelSchema', () => { expect(result).toBe(''); }); - it('should reject i18n object (no longer accepted)', () => { - expect(() => I18nLabelSchema.parse({ + // ── #5728: the inline locale map is the SECOND authorized form ────────── + // + // Ruling B (maintainer, 2026-08-06). Three published platform pages author + // 31 of these and objectui resolves them through `pickLocalized`; the schema + // declared only the plain string, so the authoritative document was the + // wrong one and the #5068 props gate reported the platform's own pages as + // broken (42 findings: 34 label + 8 `element:text.content`). + + it('accepts an inline locale map — the shape the platform pages author', () => { + const label = { en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー', 'es-ES': 'Miembros' }; + expect(I18nLabelSchema.parse(label)).toEqual(label); + }); + + it('accepts `default` as a map key — `pickLocalized` reads it as the untagged fallback', () => { + expect(I18nLabelSchema.parse({ default: 'Members', 'zh-CN': '成员' })) + .toEqual({ default: 'Members', 'zh-CN': '成员' }); + }); + + it('accepts script- and region-qualified BCP-47 tags', () => { + expect(I18nLabelSchema.safeParse({ en: 'Members', 'zh-Hans-CN': '成员', 'pt-BR': 'Membros' }).success).toBe(true); + }); + + it('rejects a map whose values are not strings', () => { + expect(() => I18nLabelSchema.parse({ en: 42 })).toThrow(); + }); + + // ── the widening does NOT resurrect the retired key-reference dialect ──── + // + // The direction that would have: a bare `z.record(z.string(), z.string())` + // accepts `{ key, defaultValue }` as "a map of strings", it parses clean, and + // `pickLocalized`'s last resort ("first string value") then renders the i18n + // KEY as the visible label in every locale. `INLINE_LOCALE_KEY` is what keeps + // these two red — the #4667 / #5055 retirement and the #5728 widening are the + // same declared = enforced principle pointing in two directions. + + it('still rejects the retired `{ key, defaultValue }` i18n object', () => { + const r = I18nLabelSchema.safeParse({ key: 'views.task_list.label', defaultValue: 'Task List', - })).toThrow(); + }); + expect(r.success).toBe(false); + // Rejected for the RIGHT reason: `defaultValue` is not a locale tag, not + // merely "the value is an object" (which the string branch would say). + expect(JSON.stringify(r.error?.issues)).toContain('invalid_key'); }); - it('should reject i18n object with params', () => { + it('still rejects the retired i18n object with params', () => { expect(() => I18nLabelSchema.parse({ key: 'common.item_count', defaultValue: '{count} items', @@ -36,12 +75,23 @@ describe('I18nLabelSchema', () => { })).toThrow(); }); - it('should reject non-string values', () => { + it('should reject non-string, non-map values', () => { expect(() => I18nLabelSchema.parse(123)).toThrow(); expect(() => I18nLabelSchema.parse(true)).toThrow(); expect(() => I18nLabelSchema.parse(null)).toThrow(); expect(() => I18nLabelSchema.parse(undefined)).toThrow(); }); + + // ── #5377 ②: the describe no longer promises keys nobody generates ─────── + it('does not claim i18n keys are auto-generated by the framework', () => { + const description = (I18nLabelSchema as unknown as { description?: string }).description ?? ''; + expect(description.length).toBeGreaterThan(0); + expect( + description, + 'no key is generated for any label — a plain string is translatable only where a resolver + ' + + 'translation slot exist for its surface (#5377)', + ).not.toMatch(/auto-generated/i); + }); }); describe('AriaPropsSchema', () => { @@ -61,16 +111,12 @@ describe('AriaPropsSchema', () => { expect(result.ariaLabel).toBe('Close dialog'); }); - it('should accept ariaLabel as string only', () => { - const props = { - ariaLabel: 'Close dialog', - }; - - const result = AriaPropsSchema.parse(props); - expect(result.ariaLabel).toBe('Close dialog'); + it('accepts ariaLabel as an inline locale map — it rides `I18nLabelSchema` (#5728)', () => { + const result = AriaPropsSchema.parse({ ariaLabel: { en: 'Close dialog', 'zh-CN': '关闭对话框' } }); + expect(result.ariaLabel).toEqual({ en: 'Close dialog', 'zh-CN': '关闭对话框' }); }); - it('should reject ariaLabel as i18n object (no longer accepted)', () => { + it('still rejects ariaLabel as the retired key-reference i18n object', () => { expect(() => AriaPropsSchema.parse({ ariaLabel: { key: 'common.close_dialog', @@ -79,6 +125,18 @@ describe('AriaPropsSchema', () => { })).toThrow(); }); + it('does not promise a generated translation key for `ariaLabel` either (#5377)', () => { + // The reporter's second surviving point. `ariaLabel` overrides the shared + // describe, so it never carried the "auto-generated" sentence verbatim — + // but it inherited the assumption, and there is no bundle slot addressing + // an `ariaLabel` anywhere in `TranslationDataSchema`. The describe now says + // so instead of leaving the author to discover it. + const shape = (AriaPropsSchema as unknown as { shape: Record }).shape; + const description = shape.ariaLabel?.description ?? ''; + expect(description).not.toMatch(/auto-generated/i); + expect(description).toMatch(/no translation-bundle slot/i); + }); + it('should accept all ARIA properties', () => { const props: AriaProps = { ariaLabel: 'Navigation menu', @@ -102,11 +160,12 @@ describe('AriaPropsSchema', () => { }); describe('I18n Integration', () => { - it('should only accept string labels', () => { + it('accepts both authorized label forms', () => { const labels: I18nLabel[] = [ 'Plain String Label', 'Another Plain String', 'Setup', + { en: 'Setup', 'zh-CN': '设置' }, ]; labels.forEach(label => { @@ -114,7 +173,7 @@ describe('I18n Integration', () => { }); }); - it('should reject i18n objects in label context', () => { + it('should reject key-reference i18n objects in label context', () => { expect(() => I18nLabelSchema.parse({ key: 'labels.translated', defaultValue: 'Translated Label' })).toThrow(); expect(() => I18nLabelSchema.parse({ key: 'labels.with_params', params: { count: 10 } })).toThrow(); }); diff --git a/packages/spec/src/ui/i18n.zod.ts b/packages/spec/src/ui/i18n.zod.ts index 0f8bf2ecfd..cd81caf724 100644 --- a/packages/spec/src/ui/i18n.zod.ts +++ b/packages/spec/src/ui/i18n.zod.ts @@ -34,13 +34,14 @@ import { z } from 'zod'; // one subtree. Leaving the two leaf shapes behind would strand exported // schemas with no consumer, which reads as a capability to whoever finds them // (#3950). -// - `I18nObjectSchema` was a vocabulary SUPERSEDED BY ITS OWN NEIGHBOUR. -// `I18nLabelSchema` below says so in prose: translation keys are generated by -// the framework at registration time from a naming convention, the author -// writes only the default-language string, and translations live in -// translation files rather than inline i18n objects. The live translation -// surface is `system/translation.zod.ts` (tightened in an earlier batch), -// which uses none of these shapes. +// - `I18nObjectSchema` was a KEY-REFERENCE dialect (`{ key, defaultValue, +// params }`) with no resolver: nothing ever looked a `key` up, so the label +// an author wrote there reached the screen as the raw key string or not at +// all. It stays retired, and #5728's widening below does NOT bring it back — +// see the locale-key constraint on {@link InlineLocaleMapSchema}, which is +// what keeps `{ key, defaultValue }` a parse error rather than a "locale map" +// whose locales happen to be named `key` and `defaultValue`. The live +// bundle-based translation surface is `system/translation.zod.ts`. // // ⚠️ Route 3 of the retirement playbook ("nothing parses it → neither"): with no // carrier key there is no shape for a `retiredKey()` tombstone to sit on and no @@ -53,21 +54,98 @@ import { z } from 'zod'; import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; +/** + * The key face of an inline locale map: a BCP-47-shaped language tag (`en`, + * `zh`, `zh-CN`, `pt-BR`, `zh-Hans-CN`), plus the literal `default`, which the + * renderer's `pickLocalized` reads as the untagged fallback entry. + * + * ## Why the key is constrained rather than left as `z.string()` + * + * A bare `z.record(z.string(), z.string())` would re-admit the **retired** + * `I18nObjectSchema` dialect through the front door: `{ key: 'views.x.label', + * defaultValue: 'Task List' }` is, structurally, a map of strings to strings. + * It would parse clean and then reach objectui's `pickLocalized`, whose last + * resort is "the first string value in the object" — so the *i18n key itself* + * renders as the visible label, in every locale. That is the silent-strip + * failure class this file's own header exists to record, arriving through the + * one shape #5055 deliberately removed. + * + * Constraining the key keeps both halves of the #5728 ruling true at once: the + * inline locale map the resolver really honours is authorized, and the dead + * key-reference dialect stays rejected — declared = enforced in both + * directions, which is exactly the #4667 consistency the ruling asks for. + * Every inline map authored in this repo (31 of them, across three platform + * pages) uses `en` / `zh-CN` / `ja-JP` / `es-ES`, so the constraint costs no + * real authoring surface. + * + * What it rejects is the retired SHAPE, not a list of banned words: the + * key-reference form always carried `defaultValue` (required on the old + * `I18nObjectSchema`), which cannot be a language tag. A hypothetical map whose + * only key is a bare three-letter `key` still parses, because nothing + * distinguishes it from a language subtag without an ISO-639 registry — and a + * hand-curated deny-list of English words that "look like" tags would be a + * claim about languages this schema has no business making. + */ +const INLINE_LOCALE_KEY = /^(default|[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*)$/; + +/** + * Inline locale map — the second authorized form of a display label. + * + * `{ en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー' }`: the author writes + * every language inline, and the renderer picks one at display time + * (objectui's `pickLocalized`, which the CLI's `i18n-extract` also recognises — + * a label already in map form is multilingual, so no bundle key is scaffolded + * for it). + */ +export const InlineLocaleMapSchema = lazySchema(() => z.record( + z.string().regex( + INLINE_LOCALE_KEY, + 'an inline label map is keyed by BCP-47 locale tags (`en`, `zh-CN`, …) or `default` — ' + + 'not by `key`/`defaultValue`, which was the retired key-reference form (#5055) and resolves to nothing', + ), + z.string(), +).describe('Inline locale map: BCP-47 tag → translated string')); + +export type InlineLocaleMap = z.input; + /** * I18n Label Schema - * - * A plain string label for display purposes. - * i18n translation keys are auto-generated by the framework at registration time - * based on a standardized naming convention (e.g., `apps...label`). - * Developers only need to provide the default-language string; translations are - * managed through translation files, not inline i18n objects. - * + * + * A display label in one of **two** authorized forms (#5728, maintainer ruling + * 2026-08-06): + * + * 1. **A plain string** — the default-language literal. Its translations live + * in a translation bundle, addressed by the convention for the surface that + * carries the label (`objects.._views..label`, + * `pages..components..label`, …) and resolved by + * `system/i18n-resolver.ts`. + * 2. **An inline locale map** — `{ en: 'Members', 'zh-CN': '成员' }`, picked at + * render time. Three published platform pages author 31 of these and + * objectui resolves them (`pickLocalized`), so the map is a delivered + * capability, not a convention the runtime ignores. + * + * Both are real; neither is deprecated by this schema. The bundle route is the + * one that scales (translators never touch `*.page.ts`) and remains the + * long-term direction — but a contract that declared only form 1 while the + * platform's own pages shipped form 2 was the authoritative document being the + * wrong one, which is what this widening fixes. + * + * ⚠️ NOT auto-generated. An earlier version of this description promised "i18n + * keys are auto-generated by the framework at registration time"; no key is + * generated for anything (#5377). A plain-string label is translatable exactly + * where the resolver and `ObjectTranslationDataSchema` define a slot for it, + * and nowhere else. + * * @example * ```typescript - * const label: I18nLabel = "All Active"; + * const plain: I18nLabel = 'All Active'; + * const inline: I18nLabel = { en: 'All Active', 'zh-CN': '全部活跃' }; * ``` */ -export const I18nLabelSchema = lazySchema(() => z.string().describe('Display label (plain string; i18n keys are auto-generated by the framework)')); +export const I18nLabelSchema = lazySchema(() => z.union([ + z.string(), + InlineLocaleMapSchema, +]).describe('Display label — the default-language string, or an inline locale map ({ en, "zh-CN" }) resolved at render time')); export type I18nLabel = z.input; @@ -140,8 +218,21 @@ export const AriaPropsSchema = lazySchema(() => strictObject({ + 'Use `ariaLabel` only if a literal accessible name is what you meant; #5058 tracks the gap.', }, }, { - /** Accessible label for screen readers */ - ariaLabel: I18nLabelSchema.optional().describe('Accessible label for screen readers (WAI-ARIA aria-label)'), + /** + * Accessible label for screen readers. + * + * Same two authorized forms as any other {@link I18nLabelSchema} label. It is + * called out here because #5377 asked whether this key inherited the + * "auto-generated i18n keys" claim: it did not carry that sentence itself, + * but it did inherit the assumption behind it. There is no generated key for + * an `ariaLabel` and no bundle slot addressing one, so a plain-string + * `ariaLabel` is announced in the source language in every locale — the + * inline map is its only localization route today. + */ + ariaLabel: I18nLabelSchema.optional().describe( + 'Accessible label for screen readers (WAI-ARIA aria-label). Plain string, or an inline locale map — ' + + 'no translation-bundle slot addresses this key, so a plain string is announced in the source language.', + ), /** ID of element that describes this component */ ariaDescribedBy: z.string().optional().describe('ID of element providing additional description (WAI-ARIA aria-describedby)'), From 7fe8f024f44a870f0af0fab438e93adc6e3e560a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 02:13:14 +0000 Subject: [PATCH 2/6] chore: changeset for the i18n label contract sweep Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY --- .../i18n-label-union-and-tab-translations.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .changeset/i18n-label-union-and-tab-translations.md diff --git a/.changeset/i18n-label-union-and-tab-translations.md b/.changeset/i18n-label-union-and-tab-translations.md new file mode 100644 index 0000000000..067ea31236 --- /dev/null +++ b/.changeset/i18n-label-union-and-tab-translations.md @@ -0,0 +1,42 @@ +--- +"@objectstack/spec": minor +"@objectstack/cli": minor +--- + +i18n label contract: inline locale maps are authorized, and filter-preset tabs get a translation slot + +**`I18nLabelSchema` accepts two forms, not one (#5728).** It declared a plain +`z.string()` while three published platform pages authored 31 inline +`{ en, 'zh-CN', 'ja-JP', 'es-ES' }` maps that objectui resolves through +`pickLocalized` — so the authoritative document was the wrong one, and the +#5068 component-props gate reported 42 findings against the platform's own +pages. The schema is now a union of the default-language string and an inline +locale map. `ElementTextPropsSchema.content` was declared a bare `z.string()` +and therefore out of that union's reach; it moves onto `I18nLabelSchema` in the +same change, which is the other 8 of the 42. The gate now reports **0**. + +This does not reverse #4667 / #5055. What those retired was the *key-reference* +dialect (`{ key, defaultValue }`) — a shape with **no resolver**, whose label +reached the screen as a raw key or not at all. What is authorized here is the +inline locale map, which has a live resolver and which the CLI's `i18n-extract` +already understands. Same "declared = enforced" principle, applied in both +directions: the map's keys are constrained to BCP-47 tags (plus `default`), so +`{ key, defaultValue }` stays a parse error rather than becoming "a locale map +whose locales are named `key` and `defaultValue`". + +Zero breaking: every previously-valid label is still valid. The +translation-bundle channel remains the direction that scales and is unchanged. + +**Filter-preset tab labels are translatable (#5377).** `ObjectTranslationData` +gains `_tabs`, addressed by `ViewTabSchema.name`, and `resolveTabLabel` reads +it — explicit `_tabs` translation, then the referenced view's `_views.*.label` +for a tab that carries `view` (the path that already worked, preserved), then +the authored literal. A tab carrying only a `filter` referenced nothing to +inherit from and had no key of its own, so its label rendered in the source +language above a fully localized grid with no authoring workaround. `os i18n +extract` scaffolds the new keys, so the slot, the resolver and the extractor +land together. + +`I18nLabelSchema`'s description no longer claims "i18n keys are auto-generated +by the framework" — none are. `AriaPropsSchema.ariaLabel` now states that no +translation-bundle slot addresses it. From 7598d7a1ba69ab415eeef4a3d3c75af41aa787cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 02:50:24 +0000 Subject: [PATCH 3/6] chore(spec): regenerate artifacts on the merge; pin InlineLocaleMap under ADR-0122 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY --- content/docs/references/api/protocol.mdx | 45 ++++++++++++++++++- content/docs/references/index.mdx | 10 ++--- ...07-unknown-key-strictness-ledger.counts.md | 2 +- .../src/type-alias-convention.pin.test.ts | 19 +++++++- 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 230b8ae77a..32f7a3192b 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -12,8 +12,8 @@ description: Protocol protocol schemas ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -1614,3 +1614,44 @@ Uninstall package response --- +## ValidateDataIssue + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | The field the finding is about (`_record` for an object-level rule). | +| **code** | `string` | ✅ | Machine-readable finding code, e.g. `required`, `invalid_type`, `rule_violation`. | +| **message** | `string` | ✅ | Human-readable message — a validation rule's author-written text where one exists. | + + +--- + +## ValidateDataRequest + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | The object name. | +| **data** | `Record \| Record[]` | ✅ | A candidate record, or an array of them. Nothing is persisted. | +| **mode** | `Enum<'insert' \| 'update'>` | optional | Which write the verdict should predict. `insert` (default) walks every declared field, so a missing required field is a finding; `update` judges only the supplied keys, matching a PATCH. | + + +--- + +## ValidateDataResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | The object name. | +| **mode** | `Enum<'insert' \| 'update'>` | ✅ | The write mode the verdict was reached for. | +| **valid** | `boolean` | ✅ | True when EVERY row is valid — the whole-set answer. | +| **results** | `{ valid: boolean; errors: { field: string; code: string; message: string }[]; warnings: { field: string; code: string; message: string }[] }[]` | ✅ | Per-row verdicts, in submission order. | +| **posture** | `{ valueShapeStrict: boolean; mediaValueShapeStrict: boolean }` | ✅ | The ADR-0104 posture the verdict was reached under — reported because it is the difference between "this row is fine" and "this row is fine HERE". The same row can be an error on a self-certified deployment and an admitted warning on an un-migrated one, and a caller explaining a verdict needs to know which it got. An unconditionally-strict preview was considered and rejected (#4633 option B): it would fail rows on every un-migrated deployment that the write would have accepted. | + + +--- + diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 0a883d0d6b..5e7a461843 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1602 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1605 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/. */} @@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 11 | 66 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 28 | 416 | REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 28 | 419 | REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 14 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. | | [Data Protocol](/docs/references/data) | 29 | 164 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | @@ -33,7 +33,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) | 37 | 295 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 147 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **200** | **1602** | 14 protocol modules | +| **Total** | **200** | **1605** | 14 protocol modules | --- @@ -61,7 +61,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **28 pages, 416 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **28 pages, 419 schemas** REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. @@ -86,7 +86,7 @@ REST/GraphQL contracts, endpoints, routing, realtime, batch, discovery. | [`odata.zod.ts`](/docs/references/api/odata) | `ODataConfig`, `ODataError`, `ODataFilterFunction`, `ODataMetadata`, `ODataQuery`, `ODataResponse` | | [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageRollbackResponse`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `HandlerStatus`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `RouteCoverageEntry`, `RouteCoverageReport`, `ValidationMode` | -| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `CreateViewRequest`, `CreateViewResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DeleteViewRequest`, `DeleteViewResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `GetViewRequest`, `GetViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `ListViewsRequest`, `ListViewsResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `UpdateViewRequest`, `UpdateViewResponse` | +| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `CreateViewRequest`, `CreateViewResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DeleteViewRequest`, `DeleteViewResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `GetViewRequest`, `GetViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `ListViewsRequest`, `ListViewsResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `UpdateViewRequest`, `UpdateViewResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | | [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` | | [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` | | [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index d887a9a5ad..821c52189e 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -266,7 +266,7 @@ directory rather than per file. | Dir | Sites | |---|---| | `ai/` | 77 | -| `api/` | 396 | +| `api/` | 401 | | `cloud/` | 83 | | `identity/` | 33 | | `integration/` | 10 | diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 8082b72c28..870984e237 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -1281,6 +1281,12 @@ export type Iso684 = Assert, z.infer< t // ui/i18n.zod.ts export type Iso686 = Assert, z.infer< typeof M161.I18nLabelSchema > >>; export type Iso687 = Assert, z.infer< typeof M161.AriaPropsSchema > >>; +// #5728 — the second arm of the widened `I18nLabelSchema`. A record of plain +// strings under a key-format constraint: the regex narrows which keys PARSE, it +// does not transform one, so the two shapes coincide and an `InlineLocaleMapParsed` +// would be a permanent synonym. Give any entry a `.default()` and this line goes +// red with the alias named — which is the point. +export type Iso758 = Assert, z.infer< typeof M161.InlineLocaleMapSchema > >>; // ui/notification.zod.ts export type Iso690 = Assert, z.infer< typeof M162.NotificationTypeSchema > >>; @@ -1444,7 +1450,10 @@ export type AFamilyParsedIsParseState = Assert< // --------------------------------------------------------------------------- describe('ADR-0122 type-alias convention', () => { - it('still declares all 751 isomorphic pins', () => { + // The title tracks the assertion below; it had been left at 751 when #6037 + // moved the count to 754, so it is corrected here rather than left two + // numbers behind. + it('still declares all 755 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 @@ -1490,9 +1499,15 @@ describe('ADR-0122 type-alias convention', () => { // 751 -> 754 is #6037's `ValidateDataIssue` / `ValidateDataRequest` / // `ValidateDataResponse` — three new protocol shapes with no defaults or // transforms anywhere in their trees, i.e. the second (RISE) case above. + // + // 754 -> 755 is #5728's `InlineLocaleMapSchema`, the second arm of the + // widened `I18nLabelSchema`. Same RISE case: a `z.record` of plain strings + // whose KEY carries a format constraint. A regex narrows which keys parse; + // it never rewrites one, so nothing in the tree produces an output shape + // the input does not already have. const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert { From ea588222606eabcbfec1df055eb00a3671084b94 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 03:12:12 +0000 Subject: [PATCH 4/6] chore(spec): regenerate skill references and the react-blocks contract The union widening changes what the generated skill artifacts say about every `I18nLabelSchema` prop (`string` -> `string | object`), and `i18n.zod.ts` gains an explicit MODULE doc so its skill-index headline is a decision rather than a side effect of which declaration sorts first. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY --- content/docs/references/ui/i18n.mdx | 17 +++++++++++++++++ packages/spec/src/ui/i18n.zod.ts | 19 +++++++++++++++++++ skills/objectstack-data/references/_index.md | 2 +- skills/objectstack-i18n/references/_index.md | 2 +- .../objectstack-platform/references/_index.md | 2 +- .../contracts/react-blocks.contract.json | 6 +++--- skills/objectstack-ui/references/_index.md | 2 +- .../objectstack-ui/references/react-blocks.md | 6 +++--- 8 files changed, 46 insertions(+), 10 deletions(-) diff --git a/content/docs/references/ui/i18n.mdx b/content/docs/references/ui/i18n.mdx index 922b29cede..2f89b877ee 100644 --- a/content/docs/references/ui/i18n.mdx +++ b/content/docs/references/ui/i18n.mdx @@ -5,6 +5,23 @@ description: I18n protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} +Display-label and ARIA-label primitives shared by every `ui/` shape. + +A MODULE doc, deliberately, rather than the first declaration's doc doing the +job by accident: `scripts/build-skill-references.ts` headlines this file in +four skill indexes from the first JSDoc it finds, so without one the headline +silently becomes whatever declaration happens to sort first. It did exactly +that when #5728 added `InlineLocaleMapSchema` above `I18nLabelSchema` — the +indexes started describing the file as a BCP-47 key format. Stating the +headline once, on the module, makes it a decision instead of a side effect of +declaration order. + +Declaration order here is load-bearing and cannot simply be flipped back: +`I18nLabelSchema` names `InlineLocaleMapSchema` in its union, and under +`OS_EAGER_SCHEMAS=1` (which `gen:schema` sets) `lazySchema` evaluates its +factory at module load — so a forward reference would be a TDZ crash in the +schema build rather than a lazy lookup. + **Source:** `packages/spec/src/ui/i18n.zod.ts` diff --git a/packages/spec/src/ui/i18n.zod.ts b/packages/spec/src/ui/i18n.zod.ts index cd81caf724..1733fe3400 100644 --- a/packages/spec/src/ui/i18n.zod.ts +++ b/packages/spec/src/ui/i18n.zod.ts @@ -54,6 +54,25 @@ import { z } from 'zod'; import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; +/** + * Display-label and ARIA-label primitives shared by every `ui/` shape. + * + * A MODULE doc, deliberately, rather than the first declaration's doc doing the + * job by accident: `scripts/build-skill-references.ts` headlines this file in + * four skill indexes from the first JSDoc it finds, so without one the headline + * silently becomes whatever declaration happens to sort first. It did exactly + * that when #5728 added `InlineLocaleMapSchema` above `I18nLabelSchema` — the + * indexes started describing the file as a BCP-47 key format. Stating the + * headline once, on the module, makes it a decision instead of a side effect of + * declaration order. + * + * Declaration order here is load-bearing and cannot simply be flipped back: + * `I18nLabelSchema` names `InlineLocaleMapSchema` in its union, and under + * `OS_EAGER_SCHEMAS=1` (which `gen:schema` sets) `lazySchema` evaluates its + * factory at module load — so a forward reference would be a TDZ crash in the + * schema build rather than a lazy lookup. + */ + /** * The key face of an inline locale map: a BCP-47-shaped language tag (`en`, * `zh`, `zh-CN`, `pt-BR`, `zh-Hans-CN`), plus the literal `default`, which the diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index d6c6bd8f6d..55276d0896 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -42,7 +42,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas - `node_modules/@objectstack/spec/src/ui/chart.zod.ts` — Unified Chart Type Taxonomy -- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Label Schema +- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol - `node_modules/@objectstack/spec/src/ui/view.zod.ts` — HTTP Method Enum & HTTP Request Schema diff --git a/skills/objectstack-i18n/references/_index.md b/skills/objectstack-i18n/references/_index.md index 3112d6ba0e..94cc0e79c5 100644 --- a/skills/objectstack-i18n/references/_index.md +++ b/skills/objectstack-i18n/references/_index.md @@ -10,7 +10,7 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas - `node_modules/@objectstack/spec/src/system/translation.zod.ts` — Shared history sentence for every shape in this file (#4001). -- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Label Schema +- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. ## Transitive dependencies diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md index fa238aa7d3..2547e517eb 100644 --- a/skills/objectstack-platform/references/_index.md +++ b/skills/objectstack-platform/references/_index.md @@ -43,7 +43,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/system/tenant.zod.ts` — Tenant Schema (Multi-Tenant Architecture) - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema - `node_modules/@objectstack/spec/src/ui/app.zod.ts` — Base Navigation Item Schema -- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Label Schema +- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. ## How to read these diff --git a/skills/objectstack-ui/contracts/react-blocks.contract.json b/skills/objectstack-ui/contracts/react-blocks.contract.json index 8810b642e5..4ed8cc3d90 100644 --- a/skills/objectstack-ui/contracts/react-blocks.contract.json +++ b/skills/objectstack-ui/contracts/react-blocks.contract.json @@ -336,21 +336,21 @@ }, { "name": "title", - "type": "string", + "type": "string | object", "kind": "data", "required": false, "description": "Chart title" }, { "name": "subtitle", - "type": "string", + "type": "string | object", "kind": "data", "required": false, "description": "Chart subtitle" }, { "name": "description", - "type": "string", + "type": "string | object", "kind": "data", "required": false, "description": "Accessibility description — announced to screen readers as the chart’s label" diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 8f7366e140..a4b50dd540 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -36,7 +36,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) - `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities - `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas -- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Label Schema +- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. - `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol diff --git a/skills/objectstack-ui/references/react-blocks.md b/skills/objectstack-ui/references/react-blocks.md index 49eed81c3a..3a49e03673 100644 --- a/skills/objectstack-ui/references/react-blocks.md +++ b/skills/objectstack-ui/references/react-blocks.md @@ -75,9 +75,9 @@ Chart over an object’s aggregated data. Bind objectName + aggregate; the axes | `drillDown` | `{ enabled?: boolean; filter?: Record; title?: string; target?: 'drawer' \| 'dialog' \| 'navigate'; columns?: string[]; maxRows?: number }` | binding | | Click a segment to open the underlying records, filtered by the clicked category, in a drawer (or 'dialog', or 'navigate' to open the object's full list page instead — that arm needs host drill navigation and falls back to the drawer without it). Present = on; {} is enough. `filter`/`title` support ${event.*} interpolation; omit `filter` to derive it from aggregate.groupBy. Declared by ChartDrillDownSchema — NOT a dashboard widget key (a dataset-bound widget drills through the semantic layer instead), and not ReportSchema.drilldown (that is lowercase, boolean, report-only). | | `filter` | `FilterArray` | controlled | | ObjectQL filter scoping the data; drive from React state. | | `type` | `'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'` | data | ✓ | | -| `title` | `string` | data | | Chart title | -| `subtitle` | `string` | data | | Chart subtitle | -| `description` | `string` | data | | Accessibility description — announced to screen readers as the chart’s label | +| `title` | `string \| object` | data | | Chart title | +| `subtitle` | `string \| object` | data | | Chart subtitle | +| `description` | `string \| object` | data | | Accessibility description — announced to screen readers as the chart’s label | | `height` | `number` | data | | Fixed plot height in pixels (overrides the container default) | | `xAxis` | `object` | data | | X-Axis configuration | | `yAxis` | `object[]` | data | | Y-Axis configuration (support dual axis) | From 976dc9bec8478e5f08357d21e75d586ed11ea560 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 04:26:04 +0000 Subject: [PATCH 5/6] chore(spec): regenerate generated surfaces from the merged tree os-regen four-step after merging origin/main: docs references, api-surface, authorable-surface (base + system). check:generated 10/10, check:spec-parsed-alias 756 OK, check:i18n-coverage none new. --- content/docs/references/index.mdx | 10 +- .../references/system/settings-manifest.mdx | 18 +-- content/docs/references/ui/dataset.mdx | 12 +- packages/spec/api-surface/system.json | 2 + packages/spec/authorable-surface.base.json | 118 +++++++----------- packages/spec/authorable-surface/system.json | 1 + 6 files changed, 67 insertions(+), 94 deletions(-) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index f7f074d784..4b93cf2186 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1605 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1606 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/. */} @@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 8 | 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) | 37 | 296 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 146 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **200** | **1605** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 147 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **200** | **1606** | 14 protocol modules | --- @@ -366,7 +366,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 146 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 147 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -380,7 +380,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ElementButtonProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementTextInputProps`, `ElementTextProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordPathProps`, `RecordRelatedListProps` | | [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` | | [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetMeasure`, `DerivedMeasureOp` | -| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel` | +| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel`, `InlineLocaleMap` | | [`notification.zod.ts`](/docs/references/ui/notification) | `NotificationPosition`, `NotificationSeverity`, `NotificationType` | | [`page.zod.ts`](/docs/references/ui/page) | `ElementDataSource`, `InterfacePageConfig`, `Page`, `PageComponent`, `PageComponentType`, `PageRegion`, `PageType`, `PageVariable` | | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | diff --git a/content/docs/references/system/settings-manifest.mdx b/content/docs/references/system/settings-manifest.mdx index 3a8518d95c..08be6df5dd 100644 --- a/content/docs/references/system/settings-manifest.mdx +++ b/content/docs/references/system/settings-manifest.mdx @@ -77,7 +77,7 @@ const result = ResolvedSettingValueSchema.parse(data); | :--- | :--- | :--- | :--- | | **namespace** | `string` | ✅ | Namespace (snake_case, globally unique) | | **version** | `integer` | optional | Manifest schema version | -| **label** | `string` | ✅ | Display label | +| **label** | `string \| Record` | ✅ | Display label | | **icon** | `string` | optional | Icon (Lucide) | | **description** | `string` | optional | Short description | | **helpText** | `string` | optional | Markdown help text shown above specifiers | @@ -86,7 +86,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **writePermission** | `string` | optional | Permission required to write | | **category** | `string` | optional | Settings hub category | | **order** | `number` | optional | Display order | -| **specifiers** | `{ type: Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| … +13 more>; id?: string; key?: string; label: string; … }[]` | ✅ | Page contents (ordered) | +| **specifiers** | `{ type: Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| … +13 more>; id?: string; key?: string; label: string \| Record; … }[]` | ✅ | Page contents (ordered) | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Whole-manifest visibility | | **featureFlag** | `string` | optional | Gate manifest visibility on a feature flag | | **beta** | `boolean` | optional | Show a Beta chip on the page | @@ -100,7 +100,7 @@ const result = ResolvedSettingValueSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **manifest** | `{ namespace: string; version?: integer; label: string; icon?: string; … }` | ✅ | | +| **manifest** | `{ namespace: string; version?: integer; label: string \| Record; icon?: string; … }` | ✅ | | | **values** | `Record; locked: boolean; lockedReason?: string; … }>` | ✅ | Effective values keyed by specifier.key | @@ -115,7 +115,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **type** | `Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| 'password' \| 'email' \| 'url' \| 'phone' \| 'number' \| 'toggle' \| 'select' \| 'radio' \| … +5 more>` | ✅ | Specifier variant | | **id** | `string` | optional | Stable identifier (snake_case) | | **key** | `string` | optional | Storage key (snake_case) | -| **label** | `string` | ✅ | Display label | +| **label** | `string \| Record` | ✅ | Display label | | **description** | `string` | optional | Help text | | **icon** | `string` | optional | Icon name (Lucide) | | **default** | `any` | optional | Default value | @@ -129,7 +129,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **writePermission** | `string` | optional | Permission required to write this specifier | | **deprecated** | `boolean` | optional | Mark deprecated | | **replacedBy** | `string` | optional | Replacement key (used when deprecated=true) | -| **options** | `{ value: string \| number \| boolean; label: string; description?: string; icon?: string }[]` | optional | Options for select/radio/multiselect | +| **options** | `{ value: string \| number \| boolean; label: string \| Record; description?: string; icon?: string }[]` | optional | Options for select/radio/multiselect | | **valueDomain** | `Enum<'iana_time_zone' \| 'iso_4217_currency' \| 'iso_3166_alpha2'>` | optional | Standard value domain enforced on write (options degrade to a UI suggestion list) | | **min** | `number` | optional | | | **max** | `number` | optional | | @@ -138,7 +138,7 @@ const result = ResolvedSettingValueSchema.parse(data); | **maxLength** | `integer` | optional | | | **pattern** | `string` | optional | Regex pattern (text only) | | **rows** | `integer` | optional | | -| **handler** | `{ kind: 'http'; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>; url: string; body?: Record; … } \| { kind: 'action'; name: string; params?: Record; confirmText?: string } \| { kind: 'navigate'; url: string; target?: Enum<'_self' \| '_blank'> }` | optional | Action handler (action_button) | +| **handler** | `{ kind: 'http'; method?: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>; url: string; body?: Record; … } \| { kind: 'action'; name: string; params?: Record; confirmText?: string \| Record } \| { kind: 'navigate'; url: string; target?: Enum<'_self' \| '_blank'> }` | optional | Action handler (action_button) | | **childNamespace** | `string` | optional | Sub-namespace (child_pane) | | **bannerText** | `string` | optional | Markdown body (info_banner) | | **bannerSeverity** | `Enum<'info' \| 'success' \| 'warning' \| 'error'>` | optional | | @@ -184,7 +184,7 @@ This schema accepts one of the following structures: | **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH'>` | ✅ | | | **url** | `string` | ✅ | Endpoint URL; supports $`{...}` interpolation | | **body** | `Record` | optional | Optional JSON body; supports $`{...}` interpolation | -| **confirmText** | `string` | optional | Confirm dialog text before invoking (omit = no confirm) | +| **confirmText** | `string \| Record` | optional | Confirm dialog text before invoking (omit = no confirm) | --- @@ -197,7 +197,7 @@ This schema accepts one of the following structures: | **kind** | `'action'` | ✅ | | | **name** | `string` | ✅ | Registered action machine name | | **params** | `Record` | optional | | -| **confirmText** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **confirmText** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | --- @@ -223,7 +223,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **value** | `string \| number \| boolean` | ✅ | Stored value | -| **label** | `string` | ✅ | Display label | +| **label** | `string \| Record` | ✅ | Display label | | **description** | `string` | optional | Optional helper text | | **icon** | `string` | optional | Optional Lucide icon name | diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index 3d4e5bbbfc..5c9f697fc2 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -49,13 +49,13 @@ const result = DatasetSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Dataset unique name | -| **label** | `string` | ✅ | Dataset label | -| **description** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | ✅ | Dataset label | +| **description** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **object** | `string` | ✅ | Base object name | | **include** | `string[]` | optional | Relationship names/paths to join (derived from object graph; max 3 hops) | | **filter** | `any` | optional | Intrinsic dataset scope filter | -| **dimensions** | `{ name: string; label?: string; field: string; type?: Enum<'string' \| 'number' \| 'date' \| 'boolean' \| 'lookup'>; … }[]` | ✅ | Groupable axes | -| **measures** | `{ name: string; label?: string; aggregate?: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; … }[]` | ✅ | Aggregatable values | +| **dimensions** | `{ name: string; label?: string \| Record; field: string; type?: Enum<'string' \| 'number' \| 'date' \| 'boolean' \| 'lookup'>; … }[]` | ✅ | Groupable axes | +| **measures** | `{ name: string; label?: string \| Record; aggregate?: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; … }[]` | ✅ | Aggregatable values | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this dataset. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | @@ -75,7 +75,7 @@ const result = DatasetSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Dimension name — referenced by presentations | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **field** | `string` | ✅ | Base field, or `relationship[.relationship].field` path | | **type** | `Enum<'string' \| 'number' \| 'date' \| 'boolean' \| 'lookup'>` | optional | | | **dateGranularity** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional | | @@ -90,7 +90,7 @@ const result = DatasetSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Measure name — e.g. "revenue"; defined once | -| **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | +| **label** | `string \| Record` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time | | **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | optional | Aggregation (sum/avg/count/...); omit when `derived` is set | | **field** | `string` | optional | Aggregated field; optional for count(*) | | **filter** | `any` | optional | | diff --git a/packages/spec/api-surface/system.json b/packages/spec/api-surface/system.json index 3594ed81bc..c898908919 100644 --- a/packages/spec/api-surface/system.json +++ b/packages/spec/api-surface/system.json @@ -749,6 +749,7 @@ "VectorClock (type)", "VectorClockSchema (const)", "ViewLike (interface)", + "ViewTabLike (interface)", "WidgetLike (interface)", "WorkerConfig (type)", "WorkerConfigParsed (type)", @@ -802,6 +803,7 @@ "resolveSettingsOptionLabel (function)", "resolveSettingsSourceLabel (function)", "resolveSettingsTitle (function)", + "resolveTabLabel (function)", "resolveViewDescription (function)", "resolveViewLabel (function)", "s3StorageExample (const)", diff --git a/packages/spec/authorable-surface.base.json b/packages/spec/authorable-surface.base.json index 171ed4e586..cfba1663bd 100644 --- a/packages/spec/authorable-surface.base.json +++ b/packages/spec/authorable-surface.base.json @@ -1,6 +1,6 @@ { "description": "In-tree anchor for the authorable-surface deletion gate (#4650, #5235): a verbatim copy of the keys in authorable-surface/ as they stood at `baseRev`, a commit on origin/main. A build that CAN reach origin/main anchors on the merge base instead, and re-verifies this file against `baseRev` — so a PR that edits it to hide a deletion goes red wherever the network exists. A build that CANNOT reach GitHub (image-build stages, air-gapped, fork, historical-tag reproduction) anchors here instead of failing. Written only by `gen:schema`, only from a git-resolved baseline — never from the build that is being checked. See #5235.", - "baseRev": "44106d9e84486dc87334d526ed53fdf4595203c9", + "baseRev": "3510e4a25dc4a0a626eb232ff7e53458c2fe9bc9", "keys": [ "ai/AIModelConfig:maxTokens", "ai/AIModelConfig:model", @@ -2007,6 +2007,17 @@ "api/UserProfileResponse:error", "api/UserProfileResponse:meta", "api/UserProfileResponse:success", + "api/ValidateDataIssue:code", + "api/ValidateDataIssue:field", + "api/ValidateDataIssue:message", + "api/ValidateDataRequest:data", + "api/ValidateDataRequest:mode", + "api/ValidateDataRequest:object", + "api/ValidateDataResponse:mode", + "api/ValidateDataResponse:object", + "api/ValidateDataResponse:posture", + "api/ValidateDataResponse:results", + "api/ValidateDataResponse:valid", "api/VersionDefinition:breakingChanges", "api/VersionDefinition:deprecatedAt", "api/VersionDefinition:description", @@ -2799,6 +2810,7 @@ "cloud/ProvisionEnvironmentResponse:credential", "cloud/ProvisionEnvironmentResponse:durationMs", "cloud/ProvisionEnvironmentResponse:environment", + "cloud/ProvisionEnvironmentResponse:hostnameAssignment", "cloud/ProvisionEnvironmentResponse:warnings", "cloud/ProvisionOrganizationRequest:createdBy", "cloud/ProvisionOrganizationRequest:defaultEnvironmentDisplayName", @@ -3312,7 +3324,7 @@ "data/ExternalFieldMapping:readonly", "data/ExternalFieldMapping:source", "data/ExternalFieldMapping:target", - "data/ExternalFieldMapping:transform", + "data/ExternalFieldMapping:transform [RETIRED]", "data/ExternalFieldMapping:type", "data/ExternalLookup:caching", "data/ExternalLookup:dataSource", @@ -4051,6 +4063,7 @@ "integration/Connector:type", "integration/Connector:webhooks", "integration/ConnectorAction:description", + "integration/ConnectorAction:effect", "integration/ConnectorAction:inputSchema", "integration/ConnectorAction:key", "integration/ConnectorAction:label", @@ -4061,7 +4074,7 @@ "integration/ConnectorFieldMapping:source", "integration/ConnectorFieldMapping:syncMode", "integration/ConnectorFieldMapping:target", - "integration/ConnectorFieldMapping:transform", + "integration/ConnectorFieldMapping:transform [RETIRED]", "integration/ConnectorHealth:circuitBreaker", "integration/ConnectorHealth:healthCheck", "integration/ConnectorInstanceAPIKeyAuth:credentialRef", @@ -5155,6 +5168,13 @@ "security/CapabilityDeclaration:name", "security/CapabilityDeclaration:packageId", "security/CapabilityDeclaration:scope", + "security/CriteriaSharingRule:_lock", + "security/CriteriaSharingRule:_lockDocsUrl", + "security/CriteriaSharingRule:_lockReason", + "security/CriteriaSharingRule:_lockSource", + "security/CriteriaSharingRule:_packageId", + "security/CriteriaSharingRule:_packageVersion", + "security/CriteriaSharingRule:_provenance", "security/CriteriaSharingRule:accessLevel", "security/CriteriaSharingRule:active", "security/CriteriaSharingRule:condition", @@ -5262,6 +5282,13 @@ "security/RowLevelSecurityPolicy:priority [RETIRED]", "security/RowLevelSecurityPolicy:tags", "security/RowLevelSecurityPolicy:using", + "security/SharingRule:_lock", + "security/SharingRule:_lockDocsUrl", + "security/SharingRule:_lockReason", + "security/SharingRule:_lockSource", + "security/SharingRule:_packageId", + "security/SharingRule:_packageVersion", + "security/SharingRule:_provenance", "security/SharingRule:accessLevel", "security/SharingRule:active", "security/SharingRule:condition", @@ -5289,7 +5316,7 @@ "shared/FieldMapping:defaultValue", "shared/FieldMapping:source", "shared/FieldMapping:target", - "shared/FieldMapping:transform", + "shared/FieldMapping:transform [RETIRED]", "shared/HttpRequest:body", "shared/HttpRequest:headers", "shared/HttpRequest:method", @@ -6606,6 +6633,7 @@ "system/Specifier:scope", "system/Specifier:step", "system/Specifier:type", + "system/Specifier:valueDomain", "system/Specifier:visible", "system/Specifier:writePermission", "system/SpecifierOption:description", @@ -7023,6 +7051,7 @@ "ui/BulkActionDef:operation", "ui/BulkActionDef:params", "ui/BulkActionDef:patch", + "ui/BulkActionDef:requiredPermissions", "ui/BulkActionDef:variant", "ui/BulkActionDef:visible", "ui/BulkActionParam:default", @@ -7219,10 +7248,6 @@ "ui/DatasetMeasure:format", "ui/DatasetMeasure:label", "ui/DatasetMeasure:name", - "ui/DateFormat:dateStyle", - "ui/DateFormat:hour12", - "ui/DateFormat:timeStyle", - "ui/DateFormat:timeZone", "ui/ElementButtonProps:action", "ui/ElementButtonProps:aria", "ui/ElementButtonProps:disabled", @@ -7268,13 +7293,17 @@ "ui/ElementNumberProps:prefix", "ui/ElementNumberProps:suffix", "ui/ElementRecordPickerProps:aria", - "ui/ElementRecordPickerProps:displayField", + "ui/ElementRecordPickerProps:displayField [RETIRED]", + "ui/ElementRecordPickerProps:emptyText", "ui/ElementRecordPickerProps:filter", - "ui/ElementRecordPickerProps:multiple", + "ui/ElementRecordPickerProps:label", + "ui/ElementRecordPickerProps:labelField", + "ui/ElementRecordPickerProps:multiple [RETIRED]", "ui/ElementRecordPickerProps:object", "ui/ElementRecordPickerProps:placeholder", - "ui/ElementRecordPickerProps:searchFields", + "ui/ElementRecordPickerProps:searchFields [RETIRED]", "ui/ElementRecordPickerProps:targetVariable", + "ui/ElementRecordPickerProps:valueField", "ui/ElementTextInputProps:aria", "ui/ElementTextInputProps:defaultValue", "ui/ElementTextInputProps:description", @@ -7413,9 +7442,6 @@ "ui/HttpRequest:method", "ui/HttpRequest:params", "ui/HttpRequest:url", - "ui/I18nObject:defaultValue", - "ui/I18nObject:key", - "ui/I18nObject:params", "ui/InlineAction:confirmText", "ui/InlineAction:errorMessage", "ui/InlineAction:label", @@ -7522,11 +7548,6 @@ "ui/ListView:userActions", "ui/ListView:userFilters", "ui/ListView:virtualScroll", - "ui/LocaleConfig:code", - "ui/LocaleConfig:dateFormat", - "ui/LocaleConfig:direction", - "ui/LocaleConfig:fallbackChain", - "ui/LocaleConfig:numberFormat", "ui/NavigationArea:description", "ui/NavigationArea:icon", "ui/NavigationArea:id", @@ -7542,12 +7563,6 @@ "ui/NavigationContribution:group", "ui/NavigationContribution:items", "ui/NavigationContribution:priority", - "ui/NumberFormat:currency", - "ui/NumberFormat:maximumFractionDigits", - "ui/NumberFormat:minimumFractionDigits", - "ui/NumberFormat:style", - "ui/NumberFormat:unit", - "ui/NumberFormat:useGrouping", "ui/ObjectListView:addRecord", "ui/ObjectListView:allowPrinting", "ui/ObjectListView:appearance", @@ -7643,8 +7658,9 @@ "ui/PageAccordionProps:items", "ui/PageCardProps:actions", "ui/PageCardProps:aria", - "ui/PageCardProps:body", + "ui/PageCardProps:body [RETIRED]", "ui/PageCardProps:bordered", + "ui/PageCardProps:children", "ui/PageCardProps:footer", "ui/PageCardProps:title", "ui/PageComponent:aria", @@ -7660,6 +7676,7 @@ "ui/PageComponent:type", "ui/PageComponent:visibility", "ui/PageComponent:visibleWhen", + "ui/PageContainerProps:children", "ui/PageHeaderProps:actions", "ui/PageHeaderProps:aria", "ui/PageHeaderProps:breadcrumb", @@ -7692,13 +7709,6 @@ "ui/PageVariable:type", "ui/PaginationConfig:pageSize", "ui/PaginationConfig:pageSizeOptions", - "ui/PluralRule:few", - "ui/PluralRule:key", - "ui/PluralRule:many", - "ui/PluralRule:one", - "ui/PluralRule:other", - "ui/PluralRule:two", - "ui/PluralRule:zero", "ui/RecordActivityProps:aria", "ui/RecordActivityProps:enableMentions", "ui/RecordActivityProps:enableReactions", @@ -7902,46 +7912,6 @@ "ui/ViewTab:order", "ui/ViewTab:pinned", "ui/ViewTab:view", - "ui/ViewTab:visible", - "ui/WidgetEvent:bubbles", - "ui/WidgetEvent:cancelable", - "ui/WidgetEvent:description", - "ui/WidgetEvent:label", - "ui/WidgetEvent:name", - "ui/WidgetEvent:payload", - "ui/WidgetLifecycle:onBlur", - "ui/WidgetLifecycle:onError", - "ui/WidgetLifecycle:onFocus", - "ui/WidgetLifecycle:onMount", - "ui/WidgetLifecycle:onUnmount", - "ui/WidgetLifecycle:onUpdate", - "ui/WidgetLifecycle:onValidate", - "ui/WidgetManifest:aria", - "ui/WidgetManifest:author", - "ui/WidgetManifest:category", - "ui/WidgetManifest:dependencies", - "ui/WidgetManifest:description", - "ui/WidgetManifest:documentation", - "ui/WidgetManifest:events", - "ui/WidgetManifest:fieldTypes", - "ui/WidgetManifest:icon", - "ui/WidgetManifest:implementation", - "ui/WidgetManifest:label", - "ui/WidgetManifest:license", - "ui/WidgetManifest:lifecycle", - "ui/WidgetManifest:name", - "ui/WidgetManifest:performance [RETIRED]", - "ui/WidgetManifest:properties", - "ui/WidgetManifest:screenshots", - "ui/WidgetManifest:tags", - "ui/WidgetManifest:version", - "ui/WidgetProperty:category", - "ui/WidgetProperty:default", - "ui/WidgetProperty:description", - "ui/WidgetProperty:label", - "ui/WidgetProperty:name", - "ui/WidgetProperty:required", - "ui/WidgetProperty:type", - "ui/WidgetProperty:validation" + "ui/ViewTab:visible" ] } diff --git a/packages/spec/authorable-surface/system.json b/packages/spec/authorable-surface/system.json index 27ad36886f..4b6317f5b9 100644 --- a/packages/spec/authorable-surface/system.json +++ b/packages/spec/authorable-surface/system.json @@ -885,6 +885,7 @@ "system/ObjectStorageConfig:scope", "system/ObjectTranslationData:_actions", "system/ObjectTranslationData:_sections", + "system/ObjectTranslationData:_tabs", "system/ObjectTranslationData:_views", "system/ObjectTranslationData:description", "system/ObjectTranslationData:fields", From 5c43911039ff60271bf9980de5080770711bc17f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 05:02:09 +0000 Subject: [PATCH 6/6] chore(spec): regenerate generated surfaces from the post-sweep merged tree Second regen round after #6526 merged: docs references, api-surface, authorable-surface, strictness-ledger counts. check:generated 10/10, check:spec-parsed-alias 749 OK, pin test 3/3. --- content/docs/references/api/protocol.mdx | 6 +- content/docs/references/index.mdx | 10 +-- ...07-unknown-key-strictness-ledger.counts.md | 2 +- packages/spec/api-surface/system.json | 2 + packages/spec/authorable-surface.base.json | 77 +------------------ packages/spec/authorable-surface/system.json | 1 + 6 files changed, 13 insertions(+), 85 deletions(-) diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 4aaae09b9f..129f9966b6 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -909,11 +909,11 @@ Get package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. | -| **label** | `string` | optional | Human-readable label shown in metadata lists. | +| **label** | `string \| Record` | optional | Human-readable label shown in metadata lists. | | **object** | `string` | optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. | -| **list** | `{ name?: string; label?: string; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | optional | | +| **list** | `{ name?: string; label?: string \| Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }` | optional | | | **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | | -| **listViews** | `Record; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | +| **listViews** | `Record; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) | | **formViews** | `Record; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 7ba22b6797..94114bf1b9 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1582 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1583 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/. */} @@ -32,8 +32,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 8 | 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) | 37 | 292 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 146 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **199** | **1582** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 147 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **199** | **1583** | 14 protocol modules | --- @@ -365,7 +365,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 146 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 147 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -379,7 +379,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ElementButtonProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementTextInputProps`, `ElementTextProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordPathProps`, `RecordRelatedListProps` | | [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` | | [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetMeasure`, `DerivedMeasureOp` | -| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel` | +| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel`, `InlineLocaleMap` | | [`notification.zod.ts`](/docs/references/ui/notification) | `NotificationPosition`, `NotificationSeverity`, `NotificationType` | | [`page.zod.ts`](/docs/references/ui/page) | `ElementDataSource`, `InterfacePageConfig`, `Page`, `PageComponent`, `PageComponentType`, `PageRegion`, `PageType`, `PageVariable` | | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index fb2fa60a11..09d461a238 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -271,4 +271,4 @@ directory rather than per file. | `kernel/` | 319 | | `qa/` | 6 | | `shared/` | 20 | -| `system/` | 361 | +| `system/` | 362 | diff --git a/packages/spec/api-surface/system.json b/packages/spec/api-surface/system.json index 64c57c287b..5aa4e30030 100644 --- a/packages/spec/api-surface/system.json +++ b/packages/spec/api-surface/system.json @@ -741,6 +741,7 @@ "VectorClock (type)", "VectorClockSchema (const)", "ViewLike (interface)", + "ViewTabLike (interface)", "WidgetLike (interface)", "WorkerConfig (type)", "WorkerConfigParsed (type)", @@ -794,6 +795,7 @@ "resolveSettingsOptionLabel (function)", "resolveSettingsSourceLabel (function)", "resolveSettingsTitle (function)", + "resolveTabLabel (function)", "resolveViewDescription (function)", "resolveViewLabel (function)", "s3StorageExample (const)", diff --git a/packages/spec/authorable-surface.base.json b/packages/spec/authorable-surface.base.json index cfba1663bd..907acf8863 100644 --- a/packages/spec/authorable-surface.base.json +++ b/packages/spec/authorable-surface.base.json @@ -1,6 +1,6 @@ { "description": "In-tree anchor for the authorable-surface deletion gate (#4650, #5235): a verbatim copy of the keys in authorable-surface/ as they stood at `baseRev`, a commit on origin/main. A build that CAN reach origin/main anchors on the merge base instead, and re-verifies this file against `baseRev` — so a PR that edits it to hide a deletion goes red wherever the network exists. A build that CANNOT reach GitHub (image-build stages, air-gapped, fork, historical-tag reproduction) anchors here instead of failing. Written only by `gen:schema`, only from a git-resolved baseline — never from the build that is being checked. See #5235.", - "baseRev": "3510e4a25dc4a0a626eb232ff7e53458c2fe9bc9", + "baseRev": "53ef05744f37789a3e2e2ee94325a616b706bea0", "keys": [ "ai/AIModelConfig:maxTokens", "ai/AIModelConfig:model", @@ -774,11 +774,6 @@ "api/CreateManyDataResponse:object", "api/CreateManyDataResponse:records", "api/CreateRequest:data", - "api/CreateViewRequest:data", - "api/CreateViewRequest:object", - "api/CreateViewResponse:object", - "api/CreateViewResponse:view", - "api/CreateViewResponse:viewId", "api/CrossObjectBatchDroppedFields:fields", "api/CrossObjectBatchDroppedFields:index", "api/CrossObjectBatchDroppedFields:object", @@ -860,11 +855,6 @@ "api/DeleteResponse:id", "api/DeleteResponse:meta", "api/DeleteResponse:success", - "api/DeleteViewRequest:object", - "api/DeleteViewRequest:viewId", - "api/DeleteViewResponse:object", - "api/DeleteViewResponse:success", - "api/DeleteViewResponse:viewId", "api/DeviceRequestResponse:code", "api/DeviceRequestResponse:expiresAt", "api/DeviceRequestResponse:interval", @@ -1157,10 +1147,6 @@ "api/GetUiViewResponse:name", "api/GetUiViewResponse:object", "api/GetUiViewResponse:protection", - "api/GetViewRequest:object", - "api/GetViewRequest:viewId", - "api/GetViewResponse:object", - "api/GetViewResponse:view", "api/HttpFindQueryParams:count", "api/HttpFindQueryParams:distinct [RETIRED]", "api/HttpFindQueryParams:expand", @@ -1347,10 +1333,6 @@ "api/ListRunsResponse:error", "api/ListRunsResponse:meta", "api/ListRunsResponse:success", - "api/ListViewsRequest:object", - "api/ListViewsRequest:type", - "api/ListViewsResponse:object", - "api/ListViewsResponse:views", "api/LoginRequest:email", "api/LoginRequest:password", "api/LoginRequest:provider", @@ -1978,12 +1960,6 @@ "api/UpdateNotificationPreferencesRequest:preferences", "api/UpdateNotificationPreferencesResponse:preferences", "api/UpdateRequest:data", - "api/UpdateViewRequest:data", - "api/UpdateViewRequest:object", - "api/UpdateViewRequest:viewId", - "api/UpdateViewResponse:object", - "api/UpdateViewResponse:view", - "api/UpdateViewResponse:viewId", "api/UploadArtifactRequest:artifact", "api/UploadArtifactRequest:releaseNotes", "api/UploadArtifactRequest:sha256", @@ -2161,41 +2137,6 @@ "automation/DeleteRecordConfig:filter", "automation/DeleteRecordConfig:multi", "automation/DeleteRecordConfig:objectName", - "automation/ETLDestination:config", - "automation/ETLDestination:connector", - "automation/ETLDestination:primaryKey", - "automation/ETLDestination:type", - "automation/ETLDestination:writeMode", - "automation/ETLPipeline:description", - "automation/ETLPipeline:destination", - "automation/ETLPipeline:enabled", - "automation/ETLPipeline:label", - "automation/ETLPipeline:metadata", - "automation/ETLPipeline:name", - "automation/ETLPipeline:notifications", - "automation/ETLPipeline:retry", - "automation/ETLPipeline:schedule", - "automation/ETLPipeline:source", - "automation/ETLPipeline:syncMode", - "automation/ETLPipeline:tags", - "automation/ETLPipeline:transformations", - "automation/ETLPipelineRun:completedAt", - "automation/ETLPipelineRun:durationMs", - "automation/ETLPipelineRun:error", - "automation/ETLPipelineRun:id", - "automation/ETLPipelineRun:logs", - "automation/ETLPipelineRun:pipelineName", - "automation/ETLPipelineRun:startedAt", - "automation/ETLPipelineRun:stats", - "automation/ETLPipelineRun:status", - "automation/ETLSource:config", - "automation/ETLSource:connector", - "automation/ETLSource:incremental", - "automation/ETLSource:type", - "automation/ETLTransformation:config", - "automation/ETLTransformation:continueOnError", - "automation/ETLTransformation:name", - "automation/ETLTransformation:type", "automation/ExecutionError:code", "automation/ExecutionError:context", "automation/ExecutionError:executionId", @@ -6518,25 +6459,9 @@ "system/SecurityEventCorrelation:linkAuthToAudit", "system/SecurityEventCorrelation:linkEncryptionToAudit", "system/SecurityEventCorrelation:linkMaskingToAudit", - "system/ServerCapabilities:compression", - "system/ServerCapabilities:httpVersions", - "system/ServerCapabilities:middleware", - "system/ServerCapabilities:routeParams", - "system/ServerCapabilities:serverPush", - "system/ServerCapabilities:sse", - "system/ServerCapabilities:streaming", - "system/ServerCapabilities:websocket", - "system/ServerEvent:data", - "system/ServerEvent:timestamp", - "system/ServerEvent:type", "system/ServerRateLimitConfig:enabled", "system/ServerRateLimitConfig:maxRequests", "system/ServerRateLimitConfig:windowMs", - "system/ServerStatus:connections", - "system/ServerStatus:requests", - "system/ServerStatus:server", - "system/ServerStatus:state", - "system/ServerStatus:uptime", "system/ServiceConfig:id", "system/ServiceConfig:name", "system/ServiceConfig:options", diff --git a/packages/spec/authorable-surface/system.json b/packages/spec/authorable-surface/system.json index cf6ac808c1..240bbcd68d 100644 --- a/packages/spec/authorable-surface/system.json +++ b/packages/spec/authorable-surface/system.json @@ -885,6 +885,7 @@ "system/ObjectStorageConfig:scope", "system/ObjectTranslationData:_actions", "system/ObjectTranslationData:_sections", + "system/ObjectTranslationData:_tabs", "system/ObjectTranslationData:_views", "system/ObjectTranslationData:description", "system/ObjectTranslationData:fields",