From 943e9216f76d40c087057c8a9159d302d6eac4f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 03:09:20 +0000 Subject: [PATCH 1/2] docs(objectui): layout-dsl teaches only shapes the live schemas accept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every YAML fence on the page is now validated by check:yaml-examples: 18 tagged / 0 untagged, up from 6 tagged / 17 untagged. Each of the 17 untagged fences was refused by every live schema it could plausibly claim (re-derived by safeParse on this base); each is now either rewritten to the declared shape and tagged, or removed with a Callout recording what it taught and why it is gone. Rewritten and tagged: the two page templates (no `context:` key; `name` + `label` are required), custom span widths (`span: auto|full`, not twelfths), the field-group examples (section `columns` + per-field `span`), related lists and widgets (page components with `properties`, not form sections), the master-detail and kanban blocks (`object-master-detail-form` / `object-kanban`, not a page-level `layout:` mapping), Customer 360 (`regions` is an array), and the onboarding wizard (a `tabbed` form view). Removed rather than implemented, per ADR-0049 enforce-or-remove in the docs direction: the `steps:` wizard template, section `variant:`, related-list `mode: inline_edit`, and the `card_grid` block (whose per-breakpoint `columns` map is the same defect class already removed from this page). No packages/spec source is touched — the docs move to the spec, not the reverse. --- content/docs/protocol/objectui/layout-dsl.mdx | 629 +++++++++--------- 1 file changed, 317 insertions(+), 312 deletions(-) diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx index 58f7edec61..751946bd66 100644 --- a/content/docs/protocol/objectui/layout-dsl.mdx +++ b/content/docs/protocol/objectui/layout-dsl.mdx @@ -50,20 +50,23 @@ variable. Both tiers can embed the platform's real data components ## Layout Hierarchy +A page holds **regions**; a region holds **components**; only a form-shaped +component holds **sections**, and a section holds **fields**. Related lists and +widgets are components in their own right — they are not children of a section. + ``` Page - ├─ Header (Title, Actions, Breadcrumbs) - ├─ Main Region - │ ├─ Section 1 - │ │ ├─ Field Group - │ │ │ ├─ Field A (span: 6) - │ │ │ └─ Field B (span: 6) - │ │ └─ Widget - │ └─ Section 2 - │ └─ Related List - └─ Sidebar Region - ├─ Widget: Quick Stats - └─ Widget: Activity Feed + ├─ Region: header + │ └─ Component: page:header (title, actions, breadcrumb) + ├─ Region: main + │ ├─ Component: record:details + │ │ └─ Section + │ │ ├─ Field A (span: auto) + │ │ └─ Field B (span: full) + │ └─ Component: record:related_list + └─ Region: sidebar + ├─ Component: object-metric + └─ Component: record:activity ``` ## Page Templates @@ -72,8 +75,15 @@ Top-level layout structures that define macro organization. ### Standard Template +A page is identified by `name` + `label`, and its content lives in `regions`, +each holding typed `components`. There is no `context:` key — the binding is +`object` plus `type` (`record` here). + +{/* os:check-yaml page */} ```yaml -context: record +name: account_record +label: Account Record Page +type: record object: account template: standard regions: @@ -85,15 +95,15 @@ regions: actions: [edit, delete, share] - name: main components: - - type: page:section + - type: record:details properties: - label: Contact Information fields: [name, email, phone] - name: sidebar + width: small components: - - type: widget + - type: record:activity properties: - component: activity_feed + limit: 10 ``` **Visual Layout:** @@ -123,17 +133,20 @@ regions: High-density layout for power users (like Salesforce Service Console). +{/* os:check-yaml page */} ```yaml -context: app +name: service_console +label: Service Console +type: app template: console regions: - name: left width: small components: - - type: record:list + - type: object-grid properties: - object: case - + objectName: case + - name: right width: full components: @@ -180,47 +193,39 @@ regions: - Sales (Opportunity pipeline + details) - Helpdesk (Ticket queue + ticket details) -### Wizard Template +### There is no wizard template -Multi-step flow with progress indicator. - -```yaml -# page_template: wizard -steps: - - name: account_info - label: Account Information - fields: [company_name, industry, size] - - name: contact_details - label: Contact Details - fields: [name, email, phone] - - name: preferences - label: Preferences - fields: [newsletter, notifications] - - name: review - label: Review & Submit - type: summary -``` - -**Visual Layout:** -``` -┌────────────────────────────────────────────────────┐ -│ New Customer Setup │ -│ ● Account Info → ○ Contact → ○ Preferences → ○ │ -├────────────────────────────────────────────────────┤ -│ │ -│ Account Information │ -│ │ -│ Company Name: _______________________________ │ -│ Industry: [Select Industry ▼] │ -│ Company Size: ○ 1-10 ○ 11-50 ○ 51-200 │ -│ │ -│ [Cancel] [Next Step →] │ -└────────────────────────────────────────────────────┘ -``` + + Earlier revisions of this page documented a **Wizard Template** — a page whose + body was a `steps:` list, each step carrying its own `label` and `fields` — and + a matching `Multi-Step Onboarding Wizard` example further down. **`steps` exists + on no schema.** `PageSchema` is `.strict()`, so it is refused by name + (`Unrecognized key(s) on this page: steps`), and no other shape in the spec + declares it either. `template: wizard` compounds it quietly: `template` is a + free-form string, so that half *parses* while naming a layout template no + renderer provides. + + Both are **removed rather than implemented** — multi-step authoring is an + implementation card first, exactly like the deferred tab loading and the + virtualization block this page removed before it. To collect a long form in + stages today, use the real, declared shapes: a `tabbed` form view (every + section renders as its own tab — see [Tabs](#tabs-multi-page-layouts)), or + a [flow](/docs/protocol/automation/flow) when the steps need to branch or + persist between visits. + ## The 12-Column Grid System -All layouts use a responsive grid that divides space into 12 columns. + + **"12 columns" describes the renderer's internal grid, not an authorable span + vocabulary.** What an author declares is much narrower, and both keys are + strict: a section's `columns` is a scalar `1`–`4`, and a field's width is + `span: auto | full` (or the legacy absolute `colSpan`, also `1`–`4`). There is + **no** `span: 6` and no twelfth of anything in the metadata. The examples below + are written in the vocabulary the schemas accept. + + +All layouts use a responsive grid that divides space into columns. ### Basic Grid Layout @@ -228,12 +233,12 @@ All layouts use a responsive grid that divides space into 12 columns. ```yaml section: label: Contact Information - columns: 2 # Each field takes 6/12 columns (50% width) + columns: 2 # two fields per row (50% width each) fields: - - name # Column 1-6 - - email # Column 7-12 - - phone # Column 1-6 (new row) - - company # Column 7-12 + - name # row 1, left + - email # row 1, right + - phone # row 2, left + - company # row 2, right ``` **Rendered Grid:** @@ -247,20 +252,24 @@ section: ### Custom Span Widths +A field's width is set on the field itself, inside the section's own `fields` +list — there is no section-level `layout:` block. `span: full` takes the whole +row at any column count; omitting `span` (`auto`) lets the renderer size the +field from its widget type and the current column count. + +{/* os:check-yaml FormSectionSchema key=section */} ```yaml section: label: Product Details - layout: + columns: 3 + fields: - field: product_name - span: 12 # Full width - - field: price - span: 4 # 33% width + span: full # whole row + - field: price # auto — one of three columns - field: quantity - span: 4 # 33% width - field: total - span: 4 # 33% width - field: description - span: 12 # Full width + span: full # whole row ``` **Rendered Grid:** @@ -390,43 +399,57 @@ sections: [row-level security](/docs/permissions/rls) — both enforced on the server. -### Section Variants +### There are no section variants -```yaml -sections: - - label: Quick Summary - variant: compact # Reduced padding - columns: 4 - fields: [status, priority, assignee, due_date] - - - label: Description - variant: spacious # Extra padding - fields: [long_description] - - - label: Danger Zone - variant: danger # Red border, warning icon - fields: [archive, delete] -``` + + Earlier revisions of this page documented a section-level `variant:` key with + the values `compact` / `spacious` / `danger`. **It existed on no schema.** + `FormSectionSchema` is `.strict()` and declares no `variant`, so authoring one + is a parse failure (`Unrecognized key(s) on this form section: variant`) — not + a section that renders with default padding. It is **removed rather than + implemented**: per-section density and emphasis are a theming decision with no + renderer behind them today. A section's authorable presentation is `label`, + `description`, `columns`, `collapsible` and `collapsed`. + ## Field Groups and Inline Layout -### Inline Field Group + + **There is no `field_group`, and no section-level `layout:` list.** Earlier + revisions of this page grouped fields two ways — an inline `field_group` nested + under a section's `layout:`, and a bare `- type: field_group` list — with + twelfth-based `span:` numbers on the fields inside. None of it existed: + `FormSectionSchema` refuses `layout` by name, and `field_group` is not a type + on any schema. **Removed rather than implemented.** + + Both examples were doing something the declared shape already does — see the + replacement below. For a value that is genuinely one composite thing rather + than neighbouring fields, the grouping belongs on the **field**: a field of + type `address` (or `composite`) carries its own `fields` list. + -Group related fields on the same row: +Fields sit on the same row because the section says how many columns it has, and +a field widens itself with `span: full`: +{/* os:check-yaml FormSectionSchema[] key=sections */} ```yaml sections: - label: Name - layout: - - type: field_group - inline: true - fields: - - field: first_name - span: 6 - placeholder: First - - field: last_name - span: 6 - placeholder: Last + columns: 2 # first_name and last_name share a row + fields: + - field: first_name + placeholder: First + - field: last_name + placeholder: Last + + - label: Address + columns: 3 + fields: + - field: street + span: full # whole row above the three below + - field: city + - field: state + - field: postal_code ``` **Rendered:** @@ -435,26 +458,7 @@ Name ┌──────────────────────┬──────────────────────┐ │ First: _____________ │ Last: ______________ │ └──────────────────────┴──────────────────────┘ -``` - -### Address Field Group - -```yaml -- type: field_group - label: Address - fields: - - field: street - span: 12 - - field: city - span: 6 - - field: state - span: 3 - - field: postal_code - span: 3 -``` -**Rendered:** -``` Address ┌─────────────────────────────────────────────────┐ │ Street: _______________________________________│ @@ -588,23 +592,27 @@ CSS-property map with camelCase keys. Prefer design tokens Display child records within a parent record's page. +**A related list is a page component, not a form section.** It lives in a page +region's `components`, with the type `record:related_list` and its configuration +under `properties`. `FormSectionSchema` refuses `type`, `object` and +`relationField` by name — a form section holds `fields`, and nothing else. + ### Basic Related List +{/* os:check-yaml PageComponentSchema */} ```yaml -sections: - - type: related_list - label: Contacts - object: contact - relationField: account_id # contact.account_id → account.id - columns: - - name - - email - - phone - actions: - - type: standard_new - label: New Contact +type: record:related_list +properties: + objectName: contact + relationshipField: account_id # contact.account_id → account.id + columns: [name, email, phone] + limit: 5 + actions: [standard_new] ``` +Note the spellings: `objectName` and `relationshipField` (not `object` / +`relationField`), and `actions` is a list of **action ids**, not objects. + **Rendered:** ``` ┌─ Contacts ─────────────────────────────── [+ New Contact] ─┐ @@ -618,53 +626,50 @@ sections: └─────────────────────────────────────────────────────────────┘ ``` -### Inline Editing Related List +### A related list has no inline-edit mode -```yaml -sections: - - type: related_list - label: Invoice Line Items - object: invoice_line - relationField: invoice_id - mode: inline_edit # Edit cells directly - columns: - - field: product - type: lookup - object: product - - field: quantity - type: number - editable: true - - field: unit_price - type: currency - editable: true - - field: total - type: formula - formula: quantity * unit_price - editable: false - actions: - - type: add_row - label: Add Line Item -``` + + Earlier revisions of this page documented an **Inline Editing Related List** — + `mode: inline_edit` on the list, with per-column `type` / `editable` / `formula` + definitions. **None of it existed.** `RecordRelatedListProps` is `.strict()` and + declares no `mode`; its `columns` is an array of **field-name strings**, never + column-definition objects, and the column's type, editability and formula are + properties of the field on the child object — not of the list that displays it. + **Removed rather than implemented.** + + Inline editing does exist, one component over: `record:details` declares + `inlineEdit` (renderer default on, where the object itself is editable). For an + editable child collection — the invoice-lines shape this example reached for — + the declared block is `object-master-detail-form` with a `details` entry; see + [Master-Detail Forms](#master-detail-forms) below. + ## Widgets: Embedding Rich Components Widgets are pre-built UI components that display data or provide functionality. +Like related lists, they are **page components** — there is no `type: widget` +wrapper and no `component:` / `config:` pair. The widget *is* the component +`type`, and its configuration is `properties`. ### Metric Widget +{/* os:check-yaml PageComponentSchema */} ```yaml -sections: - - type: widget - component: metric - config: - title: Open Opportunities - value: 47 - trend: +12% - trendDirection: up - icon: trending-up - color: success +type: object-metric +properties: + objectName: opportunity + label: Open Opportunities + aggregate: + field: id + function: count + icon: trending-up + colorVariant: success ``` +A metric **aggregates its object** — it has no literal `value:` key to hard-code +a number into. `colorVariant` (not `color`) is an enum: `default`, `blue`, +`teal`, `orange`, `purple`, `success`, `warning`, `danger`. + **Rendered:** ``` ┌─────────────────────┐ @@ -677,89 +682,106 @@ sections: ### Activity Feed Widget +The feed is bound to the record the page is already on, so it takes no `object` +or record filter of its own. + +{/* os:check-yaml PageComponentSchema */} ```yaml -sections: - - type: widget - component: activity_feed - config: - object: activity - filter: { related_to: '{recordId}' } - limit: 10 - showFilters: true +type: record:activity +properties: + limit: 10 + showFilterToggle: true + unifiedTimeline: true ``` ### Custom Widget +A component `type` outside the platform's own namespaces is an open extension +point — a registered custom or plugin component: + +{/* os:check-yaml PageComponentSchema */} ```yaml -sections: - - type: widget - component: custom.approval_timeline - config: - recordId: '{recordId}' - showComments: true +type: custom.approval_timeline +properties: + recordId: '{recordId}' + showComments: true ``` + + `properties` is an open bag (`Record`), so **it is the one part + of a component the spec parse does not check**. For the platform's own types the + authoring rules dispatch `ComponentPropsMap` and reject a misspelled prop; a + `custom.*` type has no entry there, so its props are validated only by whatever + registered the component. + + ## Advanced Layouts -### Master-Detail Split View + + **There is no page-level `layout:` block.** Earlier revisions of this page + described three advanced layouts as a top-level `layout:` mapping — + `split_view`, `card_grid` and `kanban`. On `PageSchema`, `layout` is an + **alias for `template`**, a plain string naming a layout template, so a mapping + written there is refused with exactly that rename hint (`Did you mean layout → + template?`). None of `split_view`, `card_grid`, `list_view` or `detail_view` is + a type on any schema. + + Two of the three describe capabilities the platform really has — they were + written at the wrong tier, and are restated below as the components that + deliver them. The third, `card_grid`, is **removed rather than implemented**: + besides having no schema, its per-breakpoint `columns: { desktop, tablet, + mobile }` map is the same breakpoint-map defect class removed from this page + before. Per-breakpoint layout is `responsiveStyles` (ADR-0065), documented + above. + -```yaml -layout: - type: split_view - orientation: horizontal # or 'vertical' - split: 60 # Left: 60%, Right: 40% - left: - type: list_view - object: opportunity - view: my_opportunities - right: - type: detail_view - object: opportunity - recordId: '{selectedRecordId}' -``` +### Master-Detail Forms -### Grid of Cards +A parent record edited alongside its child collections is the declared +`object-master-detail-form` block. `details` entries derive the foreign key and +columns from the child object's metadata when you omit them. +{/* os:check-yaml PageComponentSchema */} ```yaml -layout: - type: card_grid - columns: - desktop: 3 - tablet: 2 - mobile: 1 - cards: - - type: metric_card - title: Revenue - value: $1.2M - - type: metric_card - title: Deals - value: 47 - - type: chart_card - title: Pipeline - chartType: funnel +type: object-master-detail-form +properties: + objectName: invoice + mode: edit + formType: tabbed + fields: [invoice_number, customer, invoice_date] + details: + - title: Line Items + childObject: invoice_line + relationshipField: invoice_id + columns: [product, quantity, unit_price] ``` -### Kanban Board Layout +For the *master/detail browsing* pattern — pick a record on the left, show it on +the right — the mechanism is page state rather than a layout block: declare a +page `variables` entry whose `source` names the picker component, and read it in +sibling components as `page.`. + +### Kanban Board +A board is the `object-kanban` block. Its `columns` are **swimlane definitions** +(`{ id, title }` per `groupBy` value), not a field projection — the fields drawn +on each card are `cardFields`. + +{/* os:check-yaml PageComponentSchema */} ```yaml -layout: - type: kanban - object: project_task +type: object-kanban +properties: + objectName: project_task groupBy: status columns: - - value: todo - label: To Do - color: gray - - value: in_progress - label: In Progress - color: blue - - value: done - label: Done - color: green - cardFields: - - title - - assignee - - due_date + - id: todo + title: To Do + - id: in_progress + title: In Progress + - id: done + title: Done + cardTitle: title + cardFields: [assignee, due_date] ``` ## Layout Schema Reference @@ -931,109 +953,92 @@ element from anyone; it shows it to everyone. ### Customer 360 Page +`regions` is an **array** of named regions, each with a `components` list — not a +map keyed by region name. + +{/* os:check-yaml page */} ```yaml name: customer_360 +label: Customer 360 +type: record object: customer template: standard regions: - header: - - type: title - field: name - - type: actions - buttons: [edit, delete, share, convert] - - main: - - type: section - label: Overview - columns: 2 - fields: - - name - - status - - industry - - employee_count - - website - - phone - - - type: section - label: Key Contacts - columns: 1 - component: - - type: related_list - object: contact - relationField: account_id + - name: header + components: + - type: page:header + properties: + title: Customer + actions: [edit, delete, share] + + - name: main + components: + - type: record:details + properties: + columns: '2' + fields: [name, status, industry, employee_count, website, phone] + + - type: record:related_list + properties: + title: Key Contacts + objectName: contact + relationshipField: customer_id columns: [name, title, email, phone] - - - type: section - label: Open Opportunities - component: - - type: related_list - object: opportunity - relationField: account_id - filter: { stage: { $ne: 'closed_won' } } + + - type: record:related_list + properties: + title: Open Opportunities + objectName: opportunity + relationshipField: customer_id columns: [name, amount, close_date, stage] - - sidebar: - - type: widget - component: metric - config: - title: Total Revenue - value: $1.2M - - - type: widget - component: activity_feed - config: - limit: 10 + + - name: sidebar + width: small + components: + - type: object-metric + properties: + objectName: opportunity + label: Total Revenue + aggregate: + field: amount + function: sum + format: '$0,0' + + - type: record:activity + properties: + limit: 10 ``` -### Multi-Step Onboarding Wizard +### Staged Onboarding Form +The `steps:` wizard this example used does not exist (see [There is no wizard +template](#there-is-no-wizard-template)). The declared way to collect a long form +in stages is a **tabbed form view**: set `type: tabbed`, and each section becomes +its own tab in declaration order. + +{/* os:check-yaml FormViewSchema */} ```yaml -name: employee_onboarding -object: employee -template: wizard -steps: +type: tabbed +defaultTab: personal_info + +sections: - name: personal_info label: Personal Information - sections: - - label: Basic Details - columns: 2 - fields: - - first_name - - last_name - - email - - phone - - date_of_birth - - ssn - + columns: 2 + fields: [first_name, last_name, email, phone, date_of_birth] + - name: employment label: Employment Details - sections: - - label: Position - fields: - - job_title - - department - - manager - - start_date - - employment_type - + fields: [job_title, department, manager, start_date, employment_type] + - name: compensation label: Compensation - sections: - - label: Salary & Benefits - fields: - - base_salary - - bonus_eligible - - equity_grant - - benefits_plan - - - name: review - label: Review & Submit - type: summary - sections: - - type: summary_view - showAllFields: true + fields: [base_salary, bonus_eligible, equity_grant, benefits_plan] ``` +There is no `summary_view` review step and no `showAllFields` key; a read-only +recap is a section whose fields are marked `readonly`. + ## Performance The Layout DSL has **no** performance surface. A section declares no From 92d1a709df0936a5a3252e617fe8f4210c7153d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 03:58:46 +0000 Subject: [PATCH 2/2] docs(objectui): point the wizard Callout at the real flows route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The replacement Callout linked /docs/protocol/automation/flow, a route that does not exist — the page is content/docs/automation/flows.mdx, which 18 other links already spell /docs/automation/flows. Caught by the Check Documentation Links job (lychee --offline resolves root-relative links against content/); check:doc-anchors did not, because it judges in-page #fragments rather than route destinations. --- content/docs/protocol/objectui/layout-dsl.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx index 751946bd66..f23d0b1d4e 100644 --- a/content/docs/protocol/objectui/layout-dsl.mdx +++ b/content/docs/protocol/objectui/layout-dsl.mdx @@ -210,7 +210,7 @@ regions: virtualization block this page removed before it. To collect a long form in stages today, use the real, declared shapes: a `tabbed` form view (every section renders as its own tab — see [Tabs](#tabs-multi-page-layouts)), or - a [flow](/docs/protocol/automation/flow) when the steps need to branch or + a [flow](/docs/automation/flows) when the steps need to branch or persist between visits.