From 508ff524431bb8101607356c36e32c99abffb493 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 13:03:31 +0000 Subject: [PATCH] =?UTF-8?q?fix(spec):=20PageHeaderProps.title=20is=20optio?= =?UTF-8?q?nal=20=E2=80=94=20matches=20the=20synthesized=20header=20(#7702?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PageHeaderProps.title was required, but the platform's own synthesizer (objectui buildDefaultHeader) emits every seeded page:header with no title at all; the renderer falls through to the record-derived heading. PageHeaderProps.safeParse rejected the platform's own canonical output. Maintainer ruling 2026-08-11 (A/B accepted, sentinel option C rejected): title becomes optional, and its describe states the sanctioned spelling — title omitted => the renderer derives the heading from the record. - title: I18nLabelSchema.optional(), describe updated - pins: parses without title (synthesizer's real emission shape), a present title still validates as I18nLabelSchema, describe carries the sanctioned sentence - packages/lint: validateComponentProps no longer flags the synthesized page:header as component-props-invalid - regenerated content/docs/references/ui/component.mdx (check:generated) - changeset: @objectstack/spec minor (accepted-input surface widens) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk --- .changeset/pageheader-title-optional.md | 31 +++++++++++++++++++ content/docs/references/ui/component.mdx | 2 +- .../lint/src/validate-component-props.test.ts | 20 ++++++++++++ packages/spec/src/ui/component.test.ts | 24 ++++++++++++-- packages/spec/src/ui/component.zod.ts | 17 +++++++++- 5 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 .changeset/pageheader-title-optional.md diff --git a/.changeset/pageheader-title-optional.md b/.changeset/pageheader-title-optional.md new file mode 100644 index 0000000000..95d63afc10 --- /dev/null +++ b/.changeset/pageheader-title-optional.md @@ -0,0 +1,31 @@ +--- +"@objectstack/spec": minor +--- + +fix(spec): `PageHeaderProps.title` is optional — matches the platform's own synthesized header (#7702) + +`PageHeaderProps.title` was declared **required**, but the platform's own +synthesizer (objectui `buildDefaultHeader`) emits every seeded `page:header` +with **no `title` at all** — `{ type: 'page:header', recordChrome, …actions }`. +`PageHeaderRenderer` (`containers.tsx`) reads +`schema?.title ?? schema?.properties?.title` and, finding neither, falls +through to the record chip's own record-derived heading: a static authored +title would be wrong on every record but one. `PageHeaderProps.safeParse` +therefore rejected the platform's own canonical output with `title: Invalid +input` — invisible on the write path today (`PageComponent.properties` is an +opaque `z.record`), but a standing contradiction that surfaces the moment any +props-level validation runs against a header node (`validateComponentProps`, +#5068, is exactly that consumer). + +Maintainer ruling 2026-08-11 (accepting the spec lane's A/B recommendation, +rejecting a sentinel-value option C): `title` becomes optional, and its +describe states the sanctioned spelling — **title omitted ⇒ the renderer +derives the heading from the record**. Authors still set it explicitly on +non-record pages (dashboards, landing pages) where there is no record to +derive a heading from. + +This is a widening change: every payload that validated before (with `title`) +still validates identically, and `title`, when present, still parses as +`I18nLabelSchema` exactly as before. The only newly-accepted shape is a +`page:header` with `title` omitted — the platform's own default. Minor, not +patch, because the accepted-input surface grows. diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index a63ca03a17..db2a4dba5a 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -241,7 +241,7 @@ const result = AIChatWindowProps.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **title** | `string \| Record` | ✅ | Page title | +| **title** | `string \| Record` | optional | Page title. Omit to let the renderer derive the heading from the record (the default for record pages) — set explicitly on non-record pages (dashboard, landing) with no record to derive from. | | **subtitle** | `string \| Record` | optional | Page subtitle | | **icon** | `never` | optional | [REMOVED] `page:header` property `icon` was removed in @objectstack/spec 17.0.0 (#6946, ADR-0087 D2) — no renderer ever read it: objectui resolves `icon` only per header action (`action.icon`), never off the header's own props bag, and the component registry never published it as an input, so an authored value was accepted and dropped. Delete the key. The header's own identity is drawn by the record chrome (`recordChrome`, on by default) and each action carries its own `icon`. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **breadcrumb** | `boolean` | ✅ | Show breadcrumb | diff --git a/packages/lint/src/validate-component-props.test.ts b/packages/lint/src/validate-component-props.test.ts index 06791cf1a9..808f0c284e 100644 --- a/packages/lint/src/validate-component-props.test.ts +++ b/packages/lint/src/validate-component-props.test.ts @@ -169,6 +169,26 @@ describe('validateComponentProps — value verdicts', () => { ); }); + /** + * #7702 — `PageHeaderProps.title` used to be required while the platform's + * own synthesizer (objectui `buildDefaultHeader`) emits every seeded + * `page:header` with NO `title` at all: `{ type: 'page:header', recordChrome + * }`. `validateComponentProps` (this rule) would therefore flag the + * platform's own default header as `component-props-invalid` on every + * synthesized record page. Maintainer ruling 2026-08-11: `title` is + * optional. This pins the exact synthesized shape as clean — no `invalid` + * finding for a missing `title`, ever. + */ + it('does not flag the synthesized page:header (no title) as invalid (#7702)', () => { + const findings = validateComponentProps( + stackWith([{ type: 'page:header', properties: { recordChrome: true } }]), + ); + expect( + invalid(findings).filter((f) => f.path.endsWith('.properties.title')), + ).toEqual([]); + expect(findings).toEqual([]); + }); + /** * `ElementDataSourceSchema` is the component-node binding that "overrides * page-level object context", and objectui's element renderers read it FIRST diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index 19007d35cc..d0c70e4600 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -43,8 +43,28 @@ describe('PageHeaderProps', () => { expect(result.actions).toHaveLength(2); }); - it('should reject header without title', () => { - expect(() => PageHeaderProps.parse({})).toThrow(); + // #7702, maintainer ruling 2026-08-11: `title` is OPTIONAL. The platform's + // own synthesizer (objectui `buildDefaultHeader`) emits every seeded + // `page:header` with no `title` — the renderer falls through to the + // record-derived heading. `PageHeaderProps.safeParse` on that exact + // emission shape must succeed; it used to fail with `title: Invalid input`. + it('accepts a header without title — the synthesized shape (#7702)', () => { + // objectui `buildDefaultHeader`'s real emission: `{ type: 'page:header', + // recordChrome, ...(actions?) }` — no `title` key at all. + const result = PageHeaderProps.parse({ recordChrome: true }); + expect(result.title).toBeUndefined(); + expect(result.recordChrome).toBe(true); + }); + + it('accepts a completely empty header — every field optional or defaulted', () => { + const result = PageHeaderProps.parse({}); + expect(result.title).toBeUndefined(); + expect(result.breadcrumb).toBe(true); + }); + + it('still validates a present title as an I18nLabel', () => { + expect(() => PageHeaderProps.parse({ title: 42 })).toThrow(); + expect(PageHeaderProps.parse({ title: 'My Page' }).title).toBe('My Page'); }); }); diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 3c411bd2f4..e529fe9f16 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -222,7 +222,22 @@ export type PageContainerProps = z.input; */ export const PageHeaderProps = z.object({ - title: I18nLabelSchema.describe('Page title'), + /** + * Page title (#7702, maintainer ruling 2026-08-11 「接受你的建议,开始加速处理」 + * on the lane's A/B recommendation). OPTIONAL, not required: the platform's + * own synthesizer (objectui `buildDefaultHeader`) emits every seeded + * `page:header` with no `title` at all — `PageHeaderRenderer` + * (`containers.tsx:1013`) reads `schema?.title ?? schema?.properties?.title` + * and, finding neither, falls through to the record chip's own + * record-derived heading. A required `title` would reject the platform's + * own canonical output. Sanctioned spelling: title omitted ⇒ the renderer + * derives the heading from the record. Authors still set it explicitly for + * non-record pages (dashboards, landing pages) where there is no record to + * derive from. + */ + title: I18nLabelSchema.optional().describe( + 'Page title. Omit to let the renderer derive the heading from the record (the default for record pages) — set explicitly on non-record pages (dashboard, landing) with no record to derive from.', + ), subtitle: I18nLabelSchema.optional().describe('Page subtitle'), /** * REMOVED (#6946, maintainer ruling 2026-08-09 「全部接受」 on objectui#3829,