diff --git a/.changeset/10387-header-bar-unread-keys-retired.md b/.changeset/10387-header-bar-unread-keys-retired.md new file mode 100644 index 0000000000..0ce59da5dc --- /dev/null +++ b/.changeset/10387-header-bar-unread-keys-retired.md @@ -0,0 +1,34 @@ +--- +'@object-ui/types': minor +--- + +fix(types): retire the eight `header-bar` keys the renderer never read (objectui#10387) + +`HeaderBarSchema.title`, `logo`, `nav`, `left`, `center`, `right`, `sticky` and +`height` are retired on both faces (ADR-0049). None of them is in the spec, and +the `header-bar` renderer reads only `actions`, `crumbs`, `rightContent` and +`search`. Rendered through the real `SchemaRenderer`, a header with any of these +keys was identical to one without them. + +- The zod mirror (`HeaderBarSchema` in `@object-ui/types/zod`) now refuses each + key by name. The refusal says what the renderer reads instead. +- The TypeScript declaration types each key `never`. +- `logo` also stops disagreeing with itself: the declaration said an image URL + string and the mirror a node or node list. Neither rendered, and both are + refused now. + +Breaking, but only for documents the renderer already ignored. A `header-bar` +that carries any of the eight keys now fails validation and no longer +type-checks. To migrate, delete the key: + +- `title`: name the current page with the last entry of `crumbs`. +- `logo`: put brand content in `rightContent` or `actions`. +- `nav`: put links in `crumbs`, or use a `navigation-menu` / `sidebar` node. +- `left` / `center`: put custom content in `rightContent` or `actions`. +- `right`: use `actions` (a node list) or `rightContent` (one node). +- `sticky`: make the parent layout sticky; the header itself never was. +- `height`: remove it. The header's height is fixed by the renderer. + +The in-tree examples that used these keys (`packages/types/examples/dashboard.ts`, +the `@object-ui/types` README and the ObjectOS integration guide) now use +`crumbs` and `actions`. diff --git a/content/docs/components/navigation/header-bar.mdx b/content/docs/components/navigation/header-bar.mdx index 397663f7f9..29d8df7593 100644 --- a/content/docs/components/navigation/header-bar.mdx +++ b/content/docs/components/navigation/header-bar.mdx @@ -53,6 +53,22 @@ interface HeaderBarSchema { } ``` +### Retired keys + +`title`, `logo`, `nav`, `left`, `center`, `right`, `sticky` and `height` are retired +on this node (objectui#10387, alongside `variant` from objectui#10286). The renderer +never read any of them, so an authored value drew the same header as leaving it out. +They now fail validation by name, and the TypeScript declaration types each one +`never`. Delete the key: + +- `title` — name the current page with the last entry of `crumbs`. +- `logo` — put brand content in `rightContent` or `actions`. +- `nav` — put links in `crumbs`, or use a `navigation-menu` / `sidebar` node. +- `left` / `center` — put custom content in `rightContent` or `actions`. +- `right` — use `actions` (a node list) or `rightContent` (one node). +- `sticky` — make the parent layout sticky; the header itself never was. +- `height` — the header's height is fixed (`h-14`, `sm:h-16`). + ## Examples ### Application Header diff --git a/content/docs/guide/objectos-integration.mdx b/content/docs/guide/objectos-integration.mdx index d87efe9247..2b8bc93fa3 100644 --- a/content/docs/guide/objectos-integration.mdx +++ b/content/docs/guide/objectos-integration.mdx @@ -118,13 +118,7 @@ export function App() { template: 'header-sidebar-main', header: { type: 'header-bar', - title: 'My App', - navigation: { - items: [ - { label: 'Dashboard', path: '/', icon: 'home' }, - { label: 'Contacts', path: '/contacts', icon: 'users' } - ] - } + crumbs: [{ label: 'My App' }] }, sidebar: { type: 'navigation-menu', diff --git a/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx b/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx index 4e395d0e0d..42bbfb03f1 100644 --- a/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx +++ b/packages/components/src/__tests__/header-bar-right-content-numeric-falsy.test.tsx @@ -81,7 +81,7 @@ function renderRightContent(rightContent: unknown) { const C = ComponentRegistry.get('header-bar') as React.ComponentType; const { container } = render( - + , ); const header = container.querySelector('header'); @@ -94,7 +94,7 @@ function renderBaseline() { const C = ComponentRegistry.get('header-bar') as React.ComponentType; const { container } = render( - + , ); const header = container.querySelector('header'); @@ -128,7 +128,7 @@ describe('ui:header-bar rightContent numeric-falsy leak (objectui#9033)', () => // slot stopped admitting numbers and the rows below stopped being about // anything — ⛔ that is a declaration change, not a licence to delete them // (objectui#7105: node slots relax the RENDERER). - const parsed = HeaderBarSchemaZod.safeParse({ type: 'header-bar', title: 'H', rightContent: 0 }); + const parsed = HeaderBarSchemaZod.safeParse({ type: 'header-bar', rightContent: 0 }); expect(parsed.success).toBe(true); }); }); diff --git a/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx b/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx index 91f29a677f..0c6c1fa0ea 100644 --- a/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx +++ b/packages/components/src/renderers/__tests__/node-slot-numeric-falsy.test.tsx @@ -272,7 +272,7 @@ describe('SchemaNode slots refuse numeric-falsy authored values (objectui#9162)' function renderHeaderBar(value: unknown): string { const C = ComponentRegistry.get('header-bar', 'ui') as React.ComponentType; if (!C) throw new Error('no renderer registered for ui:header-bar'); - const schema: Record = { type: 'header-bar', title: 'H' }; + const schema: Record = { type: 'header-bar' }; if (value !== OMIT) schema.rightContent = value; render( diff --git a/packages/types/README.md b/packages/types/README.md index 55d4f12e43..0468add616 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -325,7 +325,7 @@ const page: FlexSchema = { type: 'flex', direction: 'col', children: [ - { type: 'header-bar', title: 'My App' }, + { type: 'header-bar', crumbs: [{ label: 'My App' }] }, { type: 'flex', direction: 'row', diff --git a/packages/types/examples/dashboard.ts b/packages/types/examples/dashboard.ts index f940d5a479..a85b1be42c 100644 --- a/packages/types/examples/dashboard.ts +++ b/packages/types/examples/dashboard.ts @@ -24,10 +24,8 @@ export const dashboardSchema: FlexSchema = { // Header { type: 'header-bar', - title: 'Object UI Dashboard', - logo: '/logo.svg', - sticky: true, - right: [ + crumbs: [{ label: 'Object UI Dashboard' }], + actions: [ { type: 'button', label: 'Profile', diff --git a/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts b/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts new file mode 100644 index 0000000000..d5e590089e --- /dev/null +++ b/packages/types/src/__tests__/header-bar-unread-keys-10387.test.ts @@ -0,0 +1,102 @@ +/** + * 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. + */ + +/** + * objectui#10387 — the `header-bar` keys that have no read site and no in-tree + * author retire from both faces (ADR-0049 enforce-or-remove; objectui#7759 ruling + * rule 2 and D1-(ii): the key is objectui-own, the spec is silent, and the read + * site is the truth). + * + * All eight keys the card names retire: `title`, `logo`, `nav`, `left`, + * `center`, `right`, `sticky` and `height`. A one-off probe through the real + * `SchemaRenderer` and the real registry drew the header byte-identical to its + * absence for every value tried, with `rightContent` as the lit control that did + * change the markup. Four of them (`title`, `logo`, `right`, `sticky`) had + * documentation and type-check-example authors and no read site; the PM seat + * ruled they retire too, and those authors migrated to `crumbs` / `actions`. + * + * `BaseSchema` is `.passthrough()`: an UNDECLARED key parses green unexamined. So + * every refusal here has a lit control on the same document, an unknown key that + * must stay green, which shows the refusal is the declared key's own verdict. + */ + +import { describe, it, expect } from 'vitest'; +import { HeaderBarSchema } from '../zod/navigation.zod.js'; +import type { HeaderBarSchema as HeaderBarSchemaType } from '../navigation.js'; + +const UNKNOWN_KEY = 'zzzNotAKeyAnySurfaceDeclares10387'; +const NODE = { type: 'header-bar' as const, crumbs: [{ label: 'Home' }] }; +const TEXT = { type: 'text', content: 'x' }; + +/** Values each face used to admit for the key, one per former arm. */ +const FORMER_VALUES: Record<'title' | 'logo' | 'nav' | 'left' | 'center' | 'right' | 'sticky' | 'height', unknown[]> = { + // `logo` is the key whose two faces disagreed: the declaration said a URL string, + // the mirror a node or node list. Every arm either face admitted is refused now. + title: ['My App'], + logo: ['/logo.svg', TEXT, [TEXT]], + nav: [[{ label: 'Docs', href: '/docs' }], []], + left: [TEXT, [TEXT], 'plain text'], + center: [TEXT, [TEXT], 'plain text'], + right: [TEXT, [TEXT]], + sticky: [true, false], + height: ['64px', 64], +}; + +function refusedPaths(result: { success: boolean; error?: { issues: { path: PropertyKey[] }[] } }): string[] { + return result.success ? [] : (result.error?.issues ?? []).map((i) => i.path.map(String).join('.')); +} + +describe('header-bar unread keys retire on both faces (objectui#10387)', () => { + for (const [key, values] of Object.entries(FORMER_VALUES)) { + it(`\`${key}\` is refused BY NAME for every value a former arm admitted`, () => { + for (const value of values) { + const result = HeaderBarSchema.safeParse({ ...NODE, [key]: value }); + expect(refusedPaths(result), `${key}: ${JSON.stringify(value)}`).toEqual([key]); + } + }); + + it(`\`${key}\`'s refusal says why and names what the renderer reads instead`, () => { + const result = HeaderBarSchema.safeParse({ ...NODE, [key]: values[0] }); + expect(result.success).toBe(false); + const issue = result.success ? undefined : result.error.issues[0]; + expect(issue?.code).toBe('invalid_type'); + expect(issue?.message.startsWith('REFUSED (objectui#10387, ADR-0049)')).toBe(true); + for (const read of ['actions', 'crumbs', 'rightContent', 'search']) expect(issue?.message).toContain(`\`${read}\``); + }); + } + + it('lit control: the same document without the keys, and with an undeclared key, parses', () => { + expect(HeaderBarSchema.safeParse(NODE).success).toBe(true); + expect(HeaderBarSchema.safeParse({ ...NODE, [UNKNOWN_KEY]: 'x' }).success).toBe(true); + }); + + it('the keys the renderer DOES read still parse (what the refusals point at)', () => { + const doc = { ...NODE, search: { enabled: true }, actions: [TEXT], rightContent: TEXT }; + expect(HeaderBarSchema.safeParse(doc).success).toBe(true); + }); + + it('the declaration refuses them too (compile-time)', () => { + // @ts-expect-error — `title` is `never` on the TS face. + const e: HeaderBarSchemaType = { type: 'header-bar', title: 'My App' }; + // @ts-expect-error — `logo` is `never` on the TS face. + const f: HeaderBarSchemaType = { type: 'header-bar', logo: '/logo.svg' }; + // @ts-expect-error — `right` is `never` on the TS face. + const g: HeaderBarSchemaType = { type: 'header-bar', right: [] }; + // @ts-expect-error — `sticky` is `never` on the TS face. + const h: HeaderBarSchemaType = { type: 'header-bar', sticky: true }; + // @ts-expect-error — `nav` is `never` on the TS face. + const a: HeaderBarSchemaType = { type: 'header-bar', nav: [{ label: 'Docs', href: '/docs' }] }; + // @ts-expect-error — `left` is `never` on the TS face. + const b: HeaderBarSchemaType = { type: 'header-bar', left: 'x' }; + // @ts-expect-error — `center` is `never` on the TS face. + const c: HeaderBarSchemaType = { type: 'header-bar', center: 'x' }; + // @ts-expect-error — `height` is `never` on the TS face. + const d: HeaderBarSchemaType = { type: 'header-bar', height: 64 }; + expect([a, b, c, d, e, f, g, h].every((n) => n.type === 'header-bar')).toBe(true); + }); +}); diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 4db17d08ce..7229feb346 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -427,8 +427,12 @@ * spelled "six" rots exactly as fast as one spelled `6`, it is just harder to * point a regex at. ⛔ Do not spell a live figure out again, and ⛔ do not * restate one without checking that the pin's spelling still reaches it. - * - **8 entries** in `WiderThanDeclared`, **10 keys** across them, and **11 arms** - * under those keys — split **5** SCHEMA-NODE, **5** CONCRETE, **0** MIXED, **1** unions. + * - **7 entries** in `WiderThanDeclared`, **9 keys** across them, and **9 arms** + * under those keys — split **5** SCHEMA-NODE, **4** CONCRETE, **0** MIXED, **0** unions. + * It read 8 / 10 / 11 — 5 / 5 / 0 / 1 — until objectui#10387 RETIRED + * `navigation.zod.ts#HeaderBarSchema::logo` on both faces: the `header-bar` renderer + * reads no `logo`, so the entry, its one key and BOTH its arms left together. It was + * the ledger's last two-arm key, which is why `unions` reached zero. * It read 8 / 11 / 12 — 5 / 6 / 0 / 1 — until objectui#10334 (objectui#7759 group F) * settled `complex.zod.ts#DashboardComponentSchema::dateRange` by rule 1: the spec * declares the key, so both faces now take the spec's authoring member by reference and @@ -3208,18 +3212,12 @@ interface WiderThanDeclared { * narrow. */ 'layout.zod.ts#PageNodeSchema': 'slots'; - /** - * CONCRETE. (`variant` LEFT under objectui#10286 — retired on both faces, see the - * note where its `KnownDrift` entry stood.) `logo` ENTERED under objectui#7760: the mirror is - * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` — the single-or-list - * spelling objectui#7069 called systematic — and the declaration states `logo?: - * string`. Both arms read now, and both are wider than a bare string, so an author - * may write a node here, `safeParse` returns green and `tsc` refuses it. ⛔ That card - * did not create the divergence: it made it MEASURABLE. `Unconstrained` had been - * excluding the key, because the face read `unknown` at the top level and one array - * deep. - */ - 'navigation.zod.ts#HeaderBarSchema': 'logo'; + // `navigation.zod.ts#HeaderBarSchema` recorded `logo` here (CONCRETE, two arms; ENTERED + // under objectui#7760): the mirror spelled it single-or-list `SchemaNode` and the + // declaration a bare `string`. The `header-bar` renderer reads no `logo` at all — through + // the real `SchemaRenderer` a URL, a node and a node list each drew the header + // byte-identical to its absence — and the spec does not declare the key, so objectui#10387 + // RETIRED it on both faces (`?: never` beside a `retirementTombstone`). The entry is GONE. // `overlay.zod.ts#TooltipSchema` recorded `content` here (CONCRETE; ENTERED under // objectui#7760): the mirror spelled it single-or-list and the declaration the single arm. // The renderer places `schema.content` RAW in a React child position, so the list arm parsed @@ -3355,7 +3353,6 @@ const WIDER_ARMS: Readonly< Record< string, readonly WiderArmClass[] > > = { 'complex.zod.ts#DashboardComponentSchema::globalFilters': ['CONCRETE'], 'form.zod.ts#FormSchema::layout': ['CONCRETE'], 'layout.zod.ts#PageNodeSchema::slots': ['SCHEMA-NODE'], - 'navigation.zod.ts#HeaderBarSchema::logo': ['CONCRETE', 'CONCRETE'], 'views.zod.ts#DetailViewFieldSchema::options': ['CONCRETE'], 'views.zod.ts#DetailViewSchema::fields': ['SCHEMA-NODE'], 'views.zod.ts#DetailViewSchema::sections': ['SCHEMA-NODE'], @@ -5280,7 +5277,14 @@ describe('the WIDER ledger is judged per ARM, not per key (objectui#8252)', () = // below would pass while measuring nothing; one that walked into object shapes // would report many arms everywhere and the ledger would be rewritten to match // an instrument rather than the mirrors. Neither is visible from the arity pin. + // + // The union direction reads a FIXED control slot beside the ledger's own rows: + // objectui#10387 retired the ledger's last two-arm key (`HeaderBarSchema::logo`), + // and a non-vacuity check that needs the LEDGER to hold a union would have gone + // red on a shrinking ledger rather than on a broken unwrapper. `SidebarSchema`'s + // `content` is the single-or-list spelling, two arms, outside the ledger. const measured = widerArmRows().map(({ pair, key }) => measureMirrorArms(pair, key)?.length); + measured.push(measureMirrorArms('navigation.zod.ts#SidebarSchema', 'content')?.length); expect(measured.filter((n) => n === 1).length).toBeGreaterThan(0); expect(measured.filter((n) => n !== undefined && n > 1).length).toBeGreaterThan(0); }); diff --git a/packages/types/src/navigation.ts b/packages/types/src/navigation.ts index 64c0efd3ab..9bdc37dc41 100644 --- a/packages/types/src/navigation.ts +++ b/packages/types/src/navigation.ts @@ -57,17 +57,42 @@ export interface NavLink { export interface HeaderBarSchema extends BaseSchema { type: 'header-bar'; /** - * Header title/brand + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `title`. + * + * Not in `@objectstack/spec`, so the objectui#7759 ruling makes the read + * site the truth, and there is none: the renderer's one function reads + * `actions`, `crumbs`, `rightContent` and `search` off `schema` and takes no + * spread props. Through the real `SchemaRenderer` an authored title drew the + * header byte-identical to its absence. For the current page name, use the + * last entry of `crumbs`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - title?: string; + title?: never; /** - * Brand logo image URL + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `logo`. + * + * No read site and no spec declaration. The two faces had also drifted + * apart: this declaration said an image URL `string`, the zod mirror a node + * or node array, and neither ever rendered. For brand content use + * `rightContent` or `actions`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - logo?: string; + logo?: never; /** - * Navigation links + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `nav`. + * + * Not in `@objectstack/spec`, so the objectui#7759 ruling makes the read + * site the truth, and there is none: the renderer's one function reads + * `actions`, `crumbs`, `rightContent` and `search` off `schema` and takes no + * spread props. Through the real `SchemaRenderer` an authored link list drew + * the header byte-identical to its absence. No in-tree document authored it. + * For links, use `crumbs` here, or a `navigation-menu` / `sidebar` node. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - nav?: NavLink[]; + nav?: never; /** * Breadcrumb items */ @@ -85,26 +110,55 @@ export interface HeaderBarSchema extends BaseSchema { */ rightContent?: SchemaNode; /** - * Left side content + * RETIRED (objectui#10387, ADR-0049) — `header-bar` has no left slot. + * + * Not in `@objectstack/spec`; the renderer reads only `actions`, `crumbs`, + * `rightContent` and `search` and takes no spread props, so an authored node + * rendered nothing. No in-tree document authored it. For custom content use + * `rightContent` or `actions`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - left?: SchemaNode | SchemaNode[]; + left?: never; /** - * Center content + * RETIRED (objectui#10387, ADR-0049) — `header-bar` has no center slot. + * + * Same reading as `left`: no read site, no spec declaration, no in-tree + * author. For custom content use `rightContent` or `actions`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - center?: SchemaNode | SchemaNode[]; + center?: never; /** - * Right side content + * RETIRED (objectui#10387, ADR-0049) — `header-bar` has no `right` slot. + * + * No read site and no spec declaration; the right side of the header is + * `actions` (a node list) and `rightContent` (one node), both read. An + * authored `right` rendered nothing. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - right?: SchemaNode | SchemaNode[]; + right?: never; /** - * Whether header is sticky - * @default true + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `sticky`. + * + * No read site and no spec declaration: `true` and `false` both drew the + * same, non-sticky header. To pin it, give the parent layout the sticky + * positioning. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - sticky?: boolean; + sticky?: never; /** - * Header height + * RETIRED (objectui#10387, ADR-0049) — `header-bar` reads no `height`. + * + * The header's height is fixed by the renderer's own classes (`h-14`, and + * `sm:h-16` from the `sm` breakpoint); no read of this key exists, and no + * in-tree document authored it. Not in `@objectstack/spec`. + * + * @deprecated Nothing renders it; the zod mirror refuses it by name. */ - height?: string | number; + height?: never; /** * RETIRED (objectui#10286, ADR-0049) — `header-bar` reads no `variant`. * diff --git a/packages/types/src/zod/navigation.zod.ts b/packages/types/src/zod/navigation.zod.ts index 787760e537..385141979a 100644 --- a/packages/types/src/zod/navigation.zod.ts +++ b/packages/types/src/zod/navigation.zod.ts @@ -62,9 +62,27 @@ export const BreadcrumbItemSchema = z.object({ */ export const HeaderBarSchema = BaseSchema.extend({ type: z.literal('header-bar'), - title: z.string().optional().describe('Header title'), - logo: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Logo content'), - nav: z.array(NavLinkSchema).optional().describe('Navigation links'), + title: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `title`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored title rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For the current page name use the last entry of `crumbs`.', + ), + logo: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `logo`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored logo (URL or node) rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For brand content use `rightContent` or `actions`.', + ), + nav: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `nav`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored link list rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For links use `crumbs`, or a `navigation-menu` / `sidebar` node.', + ), crumbs: z.array(BreadcrumbItemSchema).optional().describe('Breadcrumb items'), search: z.object({ enabled: z.boolean().describe('Whether search is enabled'), @@ -73,11 +91,41 @@ export const HeaderBarSchema = BaseSchema.extend({ }).optional().describe('Search configuration'), actions: z.array(SchemaNodeSchema).optional().describe('Right-side action slots'), rightContent: SchemaNodeSchema.optional().describe('Custom right content area'), - left: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Left content'), - center: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Center content'), - right: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Right content'), - sticky: z.boolean().optional().describe('Whether header is sticky'), - height: z.union([z.string(), z.number()]).optional().describe('Header height'), + left: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `left`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored node rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For custom content use `rightContent` or `actions`.', + ), + center: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `center`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored node rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. For custom content use `rightContent` or `actions`.', + ), + right: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `right`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so an authored node rendered nothing — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. The right side is `actions` (a node list) and `rightContent` (one node).', + ), + sticky: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `sticky`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so true and false drew the same non-sticky header — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. To pin the header, make its parent layout sticky.', + ), + height: retirementTombstone( + 'REFUSED (objectui#10387, ADR-0049) — `header-bar` reads no `height`: the key is not in ' + + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and ' + + '`search` off the node and takes no spread props, so every value drew the same fixed-height header — ' + + 'no error, no warning, no element. What it renders instead: `actions`, `crumbs`, ' + + '`rightContent`, `search`. The height is fixed by the renderer (`h-14` / `sm:h-16`).', + ), variant: retirementTombstone( 'REFUSED (objectui#10286, ADR-0049) — `header-bar` reads no `variant`: the key is not in ' + '`@objectstack/spec`, and its renderer reads only `actions`, `crumbs`, `rightContent` and '