diff --git a/.changeset/page-container-authorable-children-4027.md b/.changeset/page-container-authorable-children-4027.md new file mode 100644 index 0000000000..880e6a3174 --- /dev/null +++ b/.changeset/page-container-authorable-children-4027.md @@ -0,0 +1,40 @@ +--- +"@object-ui/components": patch +--- + +`page:card` publishes `children` instead of the retired `body`, and `page:section` / `page:footer` / `page:sidebar` publish the `children` slot they render + +`inputs` is the published authoring surface, not documentation: the Studio block +designer builds its panel from it, `sdui-parser`'s `gen-manifest.ts` serializes +it into `sdui.manifest.json` and `sdui-intrinsics.d.ts`, and the JSX-page +compiler builds its prop whitelist from it. Two of the `page:*` containers had +drifted from the contract in opposite directions. + +`page:card` published `{ name: 'body', type: 'slot' }`. `@objectstack/spec` +retired `PageCardProps.body` in objectstack#5775 (PR objectstack#6281, merged +2026-08-07, ADR-0087 D2) and declared `children` in its place — one composition +slot with one spelling, the same one `grid`, `flex`, `page:section` and +`page:tabs` items already use. The designer was teaching a key the contract now +rejects by name. + +`page:section`, `page:footer` and `page:sidebar` declared no `inputs` at all, so +the designer could not authorize the child list those three components exist to +render. The same upstream PR replaced their `EmptyProps` declaration with the +shared `PageContainerProps`, whose single key is `children`; all three now +publish that one slot from one shared literal, mirroring the spec's own single +definition. + +Rendering is unchanged in both directions. `PageCardRenderer` still READS `body` +first (`schema?.body ?? schema?.children`) and the three thin containers still +read `schema?.children || schema?.body`, so documents stored under the old +contract keep rendering until the ADR-0087 D2 conversion rewrites the key at +load time — a back-compat read is not a second authorable spelling, the same +split the `page-header-subtitle-alias` sequencing established. No validation +verdict moves either: `children` is already in `sdui-parser`'s `BASE_PROPS` (so +it was never an `unknown-prop`), `isContainer: true` was already set on all +four, and `codegen.ts` filters `slot` inputs out of the generated `.d.ts` where +`SduiBaseProps.children` types it. + +What changes for an author is the designer surface: `body` is no longer offered +on `page:card`, `children` is, and the three thin containers gained an +authorable content slot. diff --git a/apps/console/src/__tests__/public-contract.test.ts b/apps/console/src/__tests__/public-contract.test.ts index 2aca383d3d..f1568c1e87 100644 --- a/apps/console/src/__tests__/public-contract.test.ts +++ b/apps/console/src/__tests__/public-contract.test.ts @@ -231,8 +231,16 @@ const DELIBERATELY_UNCURATED: Record = { * separators. Listing them here (rather than exempting them ad hoc) keeps * "zero inputs" a reviewed decision: a block that grows a configurable surface * while sitting in this list fails the assertion below in the other direction. + * + * `page:section` / `page:footer` / `page:sidebar` left this list in + * objectui#4027, and the entry they left is the reason: "reads nothing beyond + * `children`" was doing double duty as "therefore declares nothing", and the + * child list is exactly what a designer has to be able to authorize. All three + * now publish that one slot, matching the shared `PageContainerProps` + * objectstack#5775 (PR objectstack#6281) gave them upstream. `element:divider` + * stays: it renders no children at all. */ -const PROP_LESS_CURATED = ['element:divider', 'page:section', 'page:footer', 'page:sidebar']; +const PROP_LESS_CURATED = ['element:divider']; describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => { const shippedBlocks = (ns: string): string[] => diff --git a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts index 5137fcb038..0a9d69daa1 100644 --- a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts +++ b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts @@ -225,7 +225,10 @@ const EXPECTED_COVERED = [ 'element:text_input', 'page:accordion', 'page:card', + 'page:footer', 'page:header', + 'page:section', + 'page:sidebar', 'page:tabs', 'record:activity', 'record:chatter', @@ -236,13 +239,21 @@ const EXPECTED_COVERED = [ ]; /** - * Registered, spec-carried, and deliberately propless. `EmptyProps` blocks - * (`page:footer`, `page:section`, `page:sidebar`, `nav:*`, `global:search`) + * Registered, spec-carried, and deliberately propless. `nav:*` / `global:search` * genuinely take no props; `element:image` / `element:metadata_viewer` / * `element:divider` / `ai:suggestion` are registered without an `inputs` list. * Either way there is no declaration for this gate to judge — but a block moving * OUT of `EXPECTED_COVERED` into here is an authoring surface that vanished, so * the list is pinned rather than derived-and-ignored. + * + * `page:footer` / `page:section` / `page:sidebar` LEFT this list in objectui#4027 + * and are now in `EXPECTED_COVERED`. They were the "`EmptyProps` blocks that + * genuinely take no props" this comment used to name — a reading the pinned + * rc.5 still supports and the contract no longer does: objectstack#5775 + * (PR objectstack#6281) replaced their `EmptyProps` entries with the shared + * `PageContainerProps`, whose one key is the `children` all three renderers have + * always rendered. Their `children` inputs are flagged by the forward direction + * below purely as a stale-pin artifact. */ const EXPECTED_WITHOUT_INPUTS = [ 'ai:suggestion', @@ -252,11 +263,27 @@ const EXPECTED_WITHOUT_INPUTS = [ 'global:search', 'nav:breadcrumb', 'nav:menu', - 'page:footer', - 'page:section', - 'page:sidebar', ]; +/** + * Covered blocks whose spec props schema legitimately resolves to ZERO keys on + * the pinned `@objectstack/spec`, so the non-empty probe guard below must not + * judge them (objectui#4027). + * + * The guard exists to catch a BROKEN reader — a `specTopLevelKeys` that stopped + * resolving `.shape` and returned `[]` for everything. These three return `[]` + * for a real reason instead: rc.5 still maps them to `EmptyProps`, while + * objectstack#5775 / PR objectstack#6281 replaced that with the shared + * `PageContainerProps` upstream. Without this carve-out the guard would report + * "spec shape did not resolve" for a shape that resolved perfectly well and is + * simply empty — a false accusation against the probe. + * + * Self-clearing, and pinned as such by `the empty-shape carve-out still + * describes an empty shape` below: once the pin moves, `children` appears in + * each shape and that assertion fails until this list is deleted. + */ +const SPEC_SHAPE_EMPTY_ON_THE_PIN = ['page:footer', 'page:section', 'page:sidebar']; + /** * Off-spec top-level inputs ACCEPTED for now, each with the reason. * Key format: `BLOCK.INPUT`. @@ -343,6 +370,40 @@ const OFF_SPEC_EXEMPTIONS: Record = { 'Already declared upstream by objectstack#5775; flagged only because the pinned @objectstack/spec@17.0.0-rc.5 predates it. Delete this entry when the pin moves.', 'element:record_picker.label': 'Already declared upstream by objectstack#5775; flagged only because the pinned @objectstack/spec@17.0.0-rc.5 predates it. Delete this entry when the pin moves.', + + // ── page container `children` — ALREADY settled upstream, stale pin only ─── + // The other half of the same upstream issue as the record_picker trio above, + // and the same stale-pin shape (objectui#4027). objectstack#5775 + // (PR objectstack#6281, merged 2026-08-07) declared `PageCardProps.children` + // as the canonical composition slot — retiring `body`, its second spelling — + // and gave `page:section` / `page:footer` / `page:sidebar` the shared + // `PageContainerProps`, whose one key is `children`, replacing the + // `EmptyProps` that had declared "zero props" for three components whose only + // job is to render a child list. Verified in that PR's merged diff + // (`packages/spec/src/ui/component.zod.ts`, `PageContainerProps` + the + // `ComponentPropsMap` entries), not from the issue's wording. + // + // The pinned `@objectstack/spec@17.0.0-rc.5` predates all of it: its + // `PageCardProps` still lists `body` and no `children`, and the three thin + // containers are still `EmptyProps` — so this gate reads four correct, + // contract-following declarations as off-spec. Nothing to do in either repo; + // the `no stale exemption` test below deletes these four for us, loudly, the + // moment the pin moves. + // + // Nothing about `children` moves a validation verdict either way, which is + // why publishing it ahead of the pin is safe: `validate.ts` lists `children` + // in `BASE_PROPS` (never an `unknown-prop`), and `codegen.ts:emitInterface` + // filters `slot` inputs out of the generated `.d.ts`, where + // `SduiBaseProps.children` already types it. The designer panel is the only + // surface that changes. + 'page:card.children': + 'Already declared upstream by objectstack#5775 / PR objectstack#6281 as the canonical card content slot (replacing the retired `body`); flagged only because the pinned @objectstack/spec@17.0.0-rc.5 predates it. objectui#4027. Delete this entry when the pin moves.', + 'page:section.children': + 'Already declared upstream by objectstack#5775 / PR objectstack#6281 via the shared `PageContainerProps` (this renderer has always rendered `schema.children`); flagged only because the pinned @objectstack/spec@17.0.0-rc.5 still maps this block to `EmptyProps`. objectui#4027. Delete this entry when the pin moves.', + 'page:footer.children': + 'Already declared upstream by objectstack#5775 / PR objectstack#6281 via the shared `PageContainerProps` (this renderer has always rendered `schema.children`); flagged only because the pinned @objectstack/spec@17.0.0-rc.5 still maps this block to `EmptyProps`. objectui#4027. Delete this entry when the pin moves.', + 'page:sidebar.children': + 'Already declared upstream by objectstack#5775 / PR objectstack#6281 via the shared `PageContainerProps` (this renderer has always rendered `schema.children`); flagged only because the pinned @objectstack/spec@17.0.0-rc.5 still maps this block to `EmptyProps`. objectui#4027. Delete this entry when the pin moves.', }; /** @@ -426,6 +487,30 @@ const UNPUBLISHED_EXEMPTIONS: Record = { 'element:record_picker.multiple': 'Retired upstream by objectstack#5775 (ADR-0087 D2 tombstone); declaring it would publish a key the spec rejects by name. Listed here only because the pinned @objectstack/spec@17.0.0-rc.5 predates the retirement. Resolves via objectui#3809, not via the pin bump.', + // ── page:card.body — retired upstream, stale pin only (1 key) ───────────── + // The fourth ADR-0087 D2 tombstone from the same upstream issue as the three + // above, and it withdraws here for the same reason: objectstack#5775 + // (PR objectstack#6281) replaced `PageCardProps.body` with `children`, the + // spelling every other container uses and the one this renderer reads + // (`containers.tsx`, `schema?.body ?? schema?.children`). Continuing to + // publish `body` was objectui#4027 — a designer teaching a key the contract + // rejects by name. + // + // The renderer's `body` READ deliberately survives the declaration's removal: + // documents stored under the old contract keep rendering until the ADR-0087 D2 + // conversion rewrites the key at load time. A back-compat read is not an + // authoring surface, so it does not belong in `inputs` — the same split the + // `page-header-subtitle-alias` sequencing already established in + // `packages/layout`. + // + // Like the record_picker trio, this entry does NOT go stale when the pin + // moves: D2 retirement replaces the entry with `z.never().optional()` rather + // than deleting it, so `Object.keys(shape)` still reports `body` as declared. + // It resolves when objectui#3809's tombstone recognition narrows + // `specTopLevelKeys`. + 'page:card.body': + 'Retired upstream by objectstack#5775 / PR objectstack#6281 (ADR-0087 D2 tombstone, converging on the `children` this renderer reads and now publishes); declaring it would publish a key the spec rejects by name — objectui#4027. Listed here only because the pinned @objectstack/spec@17.0.0-rc.5 predates the retirement. Resolves via objectui#3809, not via the pin bump.', + // ── element:record_picker.filter — a real A-class gap, out of scope here ─── // The renderer DOES read it (`record-picker.tsx:78`, `ds.filter ?? props.filter`, // into `query.$filter` at :103) and the spec DOES declare it, so by the bar @@ -481,11 +566,27 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) // would return `[]`, every input would read as off-spec, and the failure // would look like a repo-wide regression instead of a broken probe. An // empty result here means "fix the reader", not "fix the inputs". - for (const type of covered) { + for (const type of covered.filter((t) => !SPEC_SHAPE_EMPTY_ON_THE_PIN.includes(t))) { expect(specTopLevelKeys(type).length, `${type} spec shape did not resolve`).toBeGreaterThan(0); } }); + it('the empty-shape carve-out still describes an empty shape', () => { + // The carve-out above cannot be allowed to outlive its cause: these three + // resolve to `{}` because the PINNED rc.5 still maps them to `EmptyProps`, + // not because the probe is broken. The moment the pin carries + // `PageContainerProps`, `children` appears in the shape and this assertion + // fails — which is the instruction to delete the list and let the plain + // non-empty guard cover all three again. + for (const type of SPEC_SHAPE_EMPTY_ON_THE_PIN) { + expect(covered, `${type} no longer declares inputs — the carve-out is dead`).toContain(type); + expect( + specTopLevelKeys(type), + `${type}'s spec shape is no longer empty — delete it from SPEC_SHAPE_EMPTY_ON_THE_PIN`, + ).toEqual([]); + } + }); + it.each(covered)('%s declares no top-level input the spec does not accept', (type) => { const exempt = new Set(exemptedFor(type)); const unregistered = offSpecInputs(type).filter((name) => !exempt.has(name)); diff --git a/packages/components/src/__tests__/page-container-authorable-keys.test.tsx b/packages/components/src/__tests__/page-container-authorable-keys.test.tsx new file mode 100644 index 0000000000..5b7f22867b --- /dev/null +++ b/packages/components/src/__tests__/page-container-authorable-keys.test.tsx @@ -0,0 +1,150 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The `page:*` container registrations publish the composition slot the + * contract declares — and stop publishing the one it retired (objectui#4027). + * + * `inputs` is not documentation, it is the published AUTHORING surface: the + * Studio block designer builds its panel from it, `sdui-parser`'s + * `gen-manifest.ts` serializes it into `sdui.manifest.json` (the save gate and + * parser whitelist) and into `sdui-intrinsics.d.ts`, and `page.tsx:462` builds + * the JSX-page prop whitelist from it. Whatever it lists is what an author — + * an AI author above all — is TOLD is legal. + * + * Two drifts, opposite directions, both closed here: + * + * RETIRED KEY STILL OFFERED. `page:card` published `{ name: 'body', type: + * 'slot' }`. objectstack#5775 (PR objectstack#6281, merged 2026-08-07, + * ADR-0087 D2) retired `PageCardProps.body` and declared `children` in its + * place — one composition slot, one spelling, the same one `grid`, `flex`, + * `page:section` and `page:tabs` items already use. The designer was teaching + * a key the contract now rejects by name. + * + * DECLARED SLOT NOT OFFERED AT ALL. `page:section` / `page:footer` / + * `page:sidebar` registered with no `inputs` whatsoever, so the designer could + * not authorize the child list that is the entire point of those three + * components. The same PR gave all three the shared `PageContainerProps`, + * whose one key is `children`. + * + * WHY THESE ASSERTIONS ARE LITERAL AND NOT DERIVED FROM `@objectstack/spec`. + * The sibling gate `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` + * derives its expectations from `ComponentPropsMap` at runtime, which is the + * right shape for a repo-wide gate — but this repo pins + * `@objectstack/spec@^17.0.0-rc.5`, and rc.5 PREDATES #6281: its `PageCardProps` + * still lists `body` and has no `children`, and the three containers are still + * `EmptyProps`. A spec-derived assertion here would therefore pin the STALE + * contract and fail the change that follows the merged one. The parity gate + * absorbs that with stale-pin exemptions that name this issue and delete + * themselves when the pin moves; this file states the merged contract directly. + */ + +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ComponentRegistry } from '@object-ui/core'; + +// Module scope, not a hook: this import IS the registration (AGENTS.md +// §测试纪律 — an unbounded module load must not be billed to a bounded window), +// and it makes the registry state this file asserts on explicit rather than +// inherited from a setup file. +import '../index'; + +const inputsOf = (type: string) => { + const config = ComponentRegistry.getConfig(type); + if (!config) throw new Error(`\`${type}\` is not registered`); + return config.inputs ?? []; +}; + +const inputNamesOf = (type: string): string[] => inputsOf(type).map((i) => i.name); + +const THIN_CONTAINERS = ['page:section', 'page:footer', 'page:sidebar'] as const; + +describe('page:card publishes `children`, never the retired `body` (objectui#4027)', () => { + it('does not offer `body` as an authorable slot', () => { + // The direction this issue is named for. `body` is retired upstream; a + // designer offering it produces metadata the spec rejects by name. + expect(inputNamesOf('page:card')).not.toContain('body'); + }); + + it('offers `children` as the card content slot', () => { + const children = inputsOf('page:card').find((i) => i.name === 'children'); + expect(children, '`page:card` does not declare `children`').toBeTruthy(); + // `slot` rather than `array`: it is a composition target the designer drops + // blocks into, and `codegen.ts:emitInterface` filters `slot` inputs out of + // the generated `.d.ts` where `SduiBaseProps.children` already types it. + expect(children?.type).toBe('slot'); + }); + + it('leaves the card\'s other slots and props alone', () => { + // Scope guard: this change respells ONE slot. `footer` is a genuinely + // distinct slot and the spec keeps it; `title` / `bordered` are untouched. + expect(inputNamesOf('page:card')).toEqual(['title', 'bordered', 'children', 'footer']); + }); +}); + +describe('the thin containers publish the `children` slot they render (objectui#4027)', () => { + it.each(THIN_CONTAINERS)('%s declares exactly one input: `children`', (type) => { + expect(inputNamesOf(type)).toEqual(['children']); + expect(inputsOf(type)[0].type).toBe('slot'); + }); + + it.each(THIN_CONTAINERS)('%s is still a container', (type) => { + // The two halves are independent and both required: `isContainer` is what + // `sdui-parser/src/validate.ts` reads for its `not-a-container` diagnostic, + // and `inputs` is what the designer reads to offer the slot. Publishing one + // without the other is how objectui#3900 happened on `page:header`. + expect(ComponentRegistry.getConfig(type)?.isContainer).toBe(true); + }); + + it('shares one declaration across all three, as the spec shares one `PageContainerProps`', () => { + // Not a style point: three copied literals are three places for the + // contract to drift, which is exactly why the spec declares them once. + const [section, footer, sidebar] = THIN_CONTAINERS.map((t) => inputsOf(t)); + expect(section).toBe(footer); + expect(section).toBe(sidebar); + }); +}); + +describe('the renderers keep READING `body` — a back-compat read is not an authorable key', () => { + // The sequencing guard, and the reason removing the declaration was safe on + // its own. Documents stored against the old contract still carry `body`, and + // the ADR-0087 D2 conversion that rewrites `body` → `children` runs upstream + // at load time. Deleting the READ here — the obvious "finish the job" move — + // would blank an existing card's content silently, which is the least + // reportable failure there is. Same shape as the `page-header-subtitle-alias` + // sequencing pinned in `packages/layout`: narrow the DECLARATION now, delete + // the READ only when the conversion is live. + const renderCard = (schema: Record) => { + const Component = ComponentRegistry.get('page:card'); + if (!Component) throw new Error('page:card not registered'); + return render(); + }; + + it('renders a stored `body` on page:card', () => { + renderCard({ body: [{ type: 'text', content: 'Stored under the retired key' }] }); + expect(screen.getByText('Stored under the retired key')).toBeTruthy(); + }); + + it('renders the canonical `children` on page:card', () => { + renderCard({ children: [{ type: 'text', content: 'Authored under the canonical key' }] }); + expect(screen.getByText('Authored under the canonical key')).toBeTruthy(); + }); + + it.each(THIN_CONTAINERS)('%s renders both spellings', (type) => { + const Component = ComponentRegistry.get(type); + if (!Component) throw new Error(`${type} not registered`); + const { unmount } = render( + , + ); + expect(screen.getByText(`${type} canonical`)).toBeTruthy(); + unmount(); + + render(); + expect(screen.getByText(`${type} legacy`)).toBeTruthy(); + }); +}); diff --git a/packages/components/src/renderers/layout/containers.tsx b/packages/components/src/renderers/layout/containers.tsx index e1194979b3..b62f6e90a5 100644 --- a/packages/components/src/renderers/layout/containers.tsx +++ b/packages/components/src/renderers/layout/containers.tsx @@ -14,11 +14,13 @@ * - PageCardProps -> page:card * - PageAccordionProps -> page:accordion * - PageHeaderProps -> page:header - * - page:footer / page:sidebar / page:section thin wrappers + * - PageContainerProps -> page:footer / page:sidebar / page:section + * (the thin wrappers; one shared `children` slot) */ import React from 'react'; import { ComponentRegistry, ExpressionEvaluator, evalRowPredicate, getRecordDisplayName, toPredicateRecord } from '@object-ui/core'; +import type { ComponentInput } from '@object-ui/core'; import { actionRendersAt } from '@object-ui/types'; import { useRecordContext, useAction, useCapabilityGate, usePredicateScope, usePageVariables, useInlineEdit } from '@object-ui/react'; import { renderChildren, cn } from '../../lib/utils'; @@ -54,6 +56,41 @@ import { RecordTitleChip } from '../../custom/RecordTitleChip'; import { useObjectLabel, useSafeFieldLabel, useObjectTranslation, useSafeTranslate, createSafeTranslation, pickLocalized } from '@object-ui/i18n'; import { MoreHorizontal, RefreshCw } from 'lucide-react'; +/** + * The one authorable key the three thin containers publish — `page:section`, + * `page:footer`, `page:sidebar` (objectui#4027). + * + * `@objectstack/spec` declares all three through one shared `PageContainerProps` + * whose single key is `children` (objectstack#5775, PR objectstack#6281, merged + * 2026-08-07). They had been declared `EmptyProps` upstream — "this component + * takes zero props" — while their renderers have always rendered a child list; + * this side carried the mirror-image gap, registering all three with no `inputs` + * at all, so the designer had nothing to authorize and the generated + * `sdui.manifest.json` published them as propless. + * + * Declaring it as a `slot` is what makes this an AUTHORING-surface change and + * nothing else — no validation verdict moves in either direction: + * - `sdui-parser/src/codegen.ts:emitInterface` filters `slot` inputs out of + * `sdui-intrinsics.d.ts`, where `SduiBaseProps.children` already types it; + * - `sdui-parser/src/validate.ts` lists `children` in `BASE_PROPS`, so it was + * never an `unknown-prop` and does not become one; + * - `isContainer: true` (already set on all three) is what authorizes children + * to the parser's `not-a-container` check. + * What does move is the designer: an authorable slot on the components whose + * entire purpose is to render one. + * + * Shared rather than copied three times, mirroring the spec's own single + * `PageContainerProps` — three identical literals are three places to drift. + */ +const PAGE_CONTAINER_INPUTS: ComponentInput[] = [ + { + name: 'children', + type: 'slot', + label: 'Content', + description: 'Child components rendered inside this container, in order', + }, +]; + /** * How long the header's manual-refresh icon keeps spinning after a click * (objectui#3460). @@ -672,9 +709,21 @@ const PageCardRenderer: React.FC = ({ schema, className, ...props }) => { // page's per-plan name + price headings vanished in both locales. const title = pickLocalized(schema?.title, language); const bordered = schema?.bordered !== false; - // Accept `children` as well as `body` — every other container (grid/flex/ - // section/tabs) renders `children`, so authors expect it to work here too. - // `body` kept first for back-compat with existing card schemas. + // `children` is the authorable spelling; `body` is a READ-ONLY back-compat + // fallback for documents already stored with it (objectui#4027). + // + // `body` was retired from the contract by objectstack#5775 (PR #6281, ADR-0087 + // D2): it was a second spelling of the slot every other container — grid, flex, + // section, tabs items — calls `children`, and the spec now declares `children` + // on `PageCardProps` and rejects `body` by name. The registration below stopped + // publishing `body` in the same change, so nothing teaches it any more. + // + // The READ deliberately stays, and stays FIRST: a stored document written + // against the old contract still renders until the ADR-0087 D2 conversion + // rewrites `body` → `children` at load time. Order matters only for a document + // carrying both, which the conversion is what resolves; deleting the read + // before the conversion is live would blank an existing card's content + // silently — the `page-header-subtitle-alias` sequencing precedent, verbatim. const body = schema?.body ?? schema?.children; const footer = schema?.footer; @@ -703,7 +752,14 @@ ComponentRegistry.register('card', PageCardRenderer, { inputs: [ { name: 'title', type: 'string', label: 'Title', description: 'Accepts an inline translation map ({ en, "zh-CN", … })' }, { name: 'bordered', type: 'boolean', label: 'Bordered', defaultValue: true }, - { name: 'body', type: 'slot', label: 'Body', description: 'Card content; plain `children` also render here' }, + // The card's content slot, respelled from `body` to `children` + // (objectui#4027). One slot, one spelling: objectstack#5775 (PR #6281) + // retired `PageCardProps.body` and declared `children` in its place, so a + // designer that kept offering `body` was teaching a key the contract now + // rejects by name. The renderer still READS `body` for stored documents — + // see the comment at its read site above — but a back-compat read is not a + // second authorable spelling, and `inputs` is the authoring surface. + { name: 'children', type: 'slot', label: 'Content', description: 'Card content components, in order (the card body slot)' }, { name: 'footer', type: 'slot', label: 'Footer' }, ], }); @@ -816,6 +872,7 @@ ComponentRegistry.register('section', PageSectionRenderer, { label: 'Page Section', category: 'layout', isContainer: true, + inputs: PAGE_CONTAINER_INPUTS, }); // --------------------------------------------------------------------------- @@ -1615,6 +1672,7 @@ ComponentRegistry.register('footer', PageFooterRenderer, { label: 'Page Footer', category: 'layout', isContainer: true, + inputs: PAGE_CONTAINER_INPUTS, }); // --------------------------------------------------------------------------- @@ -1639,4 +1697,5 @@ ComponentRegistry.register('sidebar', PageSidebarRenderer, { label: 'Page Sidebar', category: 'layout', isContainer: true, + inputs: PAGE_CONTAINER_INPUTS, });