Skip to content

Commit c939055

Browse files
os-zhuangclaude
andauthored
fix(spec): PageHeaderProps.title is optional — matches the synthesized header (#7702) (#7756)
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) Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 098b629 commit c939055

5 files changed

Lines changed: 90 additions & 4 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
fix(spec): `PageHeaderProps.title` is optional — matches the platform's own synthesized header (#7702)
6+
7+
`PageHeaderProps.title` was declared **required**, but the platform's own
8+
synthesizer (objectui `buildDefaultHeader`) emits every seeded `page:header`
9+
with **no `title` at all**`{ type: 'page:header', recordChrome, …actions }`.
10+
`PageHeaderRenderer` (`containers.tsx`) reads
11+
`schema?.title ?? schema?.properties?.title` and, finding neither, falls
12+
through to the record chip's own record-derived heading: a static authored
13+
title would be wrong on every record but one. `PageHeaderProps.safeParse`
14+
therefore rejected the platform's own canonical output with `title: Invalid
15+
input` — invisible on the write path today (`PageComponent.properties` is an
16+
opaque `z.record`), but a standing contradiction that surfaces the moment any
17+
props-level validation runs against a header node (`validateComponentProps`,
18+
#5068, is exactly that consumer).
19+
20+
Maintainer ruling 2026-08-11 (accepting the spec lane's A/B recommendation,
21+
rejecting a sentinel-value option C): `title` becomes optional, and its
22+
describe states the sanctioned spelling — **title omitted ⇒ the renderer
23+
derives the heading from the record**. Authors still set it explicitly on
24+
non-record pages (dashboards, landing pages) where there is no record to
25+
derive a heading from.
26+
27+
This is a widening change: every payload that validated before (with `title`)
28+
still validates identically, and `title`, when present, still parses as
29+
`I18nLabelSchema` exactly as before. The only newly-accepted shape is a
30+
`page:header` with `title` omitted — the platform's own default. Minor, not
31+
patch, because the accepted-input surface grows.

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ const result = AIChatWindowProps.parse(data);
241241

242242
| Property | Type | Required | Description |
243243
| :--- | :--- | :--- | :--- |
244-
| **title** | `string \| Record<string, string>` | | Page title |
244+
| **title** | `string \| Record<string, string>` | 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. |
245245
| **subtitle** | `string \| Record<string, string>` | optional | Page subtitle |
246246
| **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. |
247247
| **breadcrumb** | `boolean` || Show breadcrumb |

packages/lint/src/validate-component-props.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,26 @@ describe('validateComponentProps — value verdicts', () => {
169169
);
170170
});
171171

172+
/**
173+
* #7702 — `PageHeaderProps.title` used to be required while the platform's
174+
* own synthesizer (objectui `buildDefaultHeader`) emits every seeded
175+
* `page:header` with NO `title` at all: `{ type: 'page:header', recordChrome
176+
* }`. `validateComponentProps` (this rule) would therefore flag the
177+
* platform's own default header as `component-props-invalid` on every
178+
* synthesized record page. Maintainer ruling 2026-08-11: `title` is
179+
* optional. This pins the exact synthesized shape as clean — no `invalid`
180+
* finding for a missing `title`, ever.
181+
*/
182+
it('does not flag the synthesized page:header (no title) as invalid (#7702)', () => {
183+
const findings = validateComponentProps(
184+
stackWith([{ type: 'page:header', properties: { recordChrome: true } }]),
185+
);
186+
expect(
187+
invalid(findings).filter((f) => f.path.endsWith('.properties.title')),
188+
).toEqual([]);
189+
expect(findings).toEqual([]);
190+
});
191+
172192
/**
173193
* `ElementDataSourceSchema` is the component-node binding that "overrides
174194
* page-level object context", and objectui's element renderers read it FIRST

packages/spec/src/ui/component.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,28 @@ describe('PageHeaderProps', () => {
4343
expect(result.actions).toHaveLength(2);
4444
});
4545

46-
it('should reject header without title', () => {
47-
expect(() => PageHeaderProps.parse({})).toThrow();
46+
// #7702, maintainer ruling 2026-08-11: `title` is OPTIONAL. The platform's
47+
// own synthesizer (objectui `buildDefaultHeader`) emits every seeded
48+
// `page:header` with no `title` — the renderer falls through to the
49+
// record-derived heading. `PageHeaderProps.safeParse` on that exact
50+
// emission shape must succeed; it used to fail with `title: Invalid input`.
51+
it('accepts a header without title — the synthesized shape (#7702)', () => {
52+
// objectui `buildDefaultHeader`'s real emission: `{ type: 'page:header',
53+
// recordChrome, ...(actions?) }` — no `title` key at all.
54+
const result = PageHeaderProps.parse({ recordChrome: true });
55+
expect(result.title).toBeUndefined();
56+
expect(result.recordChrome).toBe(true);
57+
});
58+
59+
it('accepts a completely empty header — every field optional or defaulted', () => {
60+
const result = PageHeaderProps.parse({});
61+
expect(result.title).toBeUndefined();
62+
expect(result.breadcrumb).toBe(true);
63+
});
64+
65+
it('still validates a present title as an I18nLabel', () => {
66+
expect(() => PageHeaderProps.parse({ title: 42 })).toThrow();
67+
expect(PageHeaderProps.parse({ title: 'My Page' }).title).toBe('My Page');
4868
});
4969
});
5070

packages/spec/src/ui/component.zod.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,22 @@ export type PageContainerProps = z.input<typeof PageContainerProps>;
222222
*/
223223

224224
export const PageHeaderProps = z.object({
225-
title: I18nLabelSchema.describe('Page title'),
225+
/**
226+
* Page title (#7702, maintainer ruling 2026-08-11 「接受你的建议,开始加速处理」
227+
* on the lane's A/B recommendation). OPTIONAL, not required: the platform's
228+
* own synthesizer (objectui `buildDefaultHeader`) emits every seeded
229+
* `page:header` with no `title` at all — `PageHeaderRenderer`
230+
* (`containers.tsx:1013`) reads `schema?.title ?? schema?.properties?.title`
231+
* and, finding neither, falls through to the record chip's own
232+
* record-derived heading. A required `title` would reject the platform's
233+
* own canonical output. Sanctioned spelling: title omitted ⇒ the renderer
234+
* derives the heading from the record. Authors still set it explicitly for
235+
* non-record pages (dashboards, landing pages) where there is no record to
236+
* derive from.
237+
*/
238+
title: I18nLabelSchema.optional().describe(
239+
'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.',
240+
),
226241
subtitle: I18nLabelSchema.optional().describe('Page subtitle'),
227242
/**
228243
* REMOVED (#6946, maintainer ruling 2026-08-09 「全部接受」 on objectui#3829,

0 commit comments

Comments
 (0)