From 485513cae85b43a4e674ea6938f0a5c1e8dc9e23 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:31:27 +0000 Subject: [PATCH] feat(components): page:header stops reading showStar; the host toggle is the one star switch The renderer read, the pass to RecordTitleChip and the registration input leave ahead of the @objectstack/spec bump that tombstones the key (objectstack#23650, landed as objectstack#23781). The chip's own showStar prop retires too: it draws its star exactly when onToggleFavorite is given. The CONTROL case that pinned "showStar: false hides the star" is replaced by pins that an authored showStar (on the node and under properties) changes nothing, and that the registration publishes no showStar input. The parity suite's settledUpstream row leaves; a page:header.showStar entry in UNPUBLISHED_EXEMPTIONS covers the installed spec, and goes dangling and stale on the bump that installs the tombstone. Claude-Session: https://claude.ai/code/session_01RP9ftxaq1x5KKbvFpDWptn Co-authored-by: Claude --- .../12627-page-header-show-star-retired.md | 9 ++++ .../registry-inputs-spec-parity.test.ts | 27 ++++++++++- content/docs/guide/layout.md | 3 +- content/docs/layout/page-header.mdx | 3 +- ...age-header-star-host-toggle-12427.test.tsx | 48 +++++++++++++++---- .../components/src/custom/RecordTitleChip.tsx | 10 ++-- .../src/renderers/layout/containers.tsx | 17 ++++--- 7 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 .changeset/12627-page-header-show-star-retired.md diff --git a/.changeset/12627-page-header-show-star-retired.md b/.changeset/12627-page-header-show-star-retired.md new file mode 100644 index 0000000000..b798ccf8db --- /dev/null +++ b/.changeset/12627-page-header-show-star-retired.md @@ -0,0 +1,9 @@ +--- +'@object-ui/components': minor +--- + +`page:header` no longer reads `showStar`, ahead of `@objectstack/spec` 18 retiring the key (objectstack#23650). + +- **The favorites star follows the host toggle alone.** The star beside the record title draws exactly where the host's record context carries `onToggleFavorite`, and metadata does not control it. An authored `showStar`, on the node or under `properties`, changes nothing. Before, `showStar: false` hid the star even where the host wired a toggle. `RecordContextValue` is unchanged: `isFavorite` and `onToggleFavorite` stay the way a host opts in. +- **The `page:header` registration no longer publishes a `showStar` input**, so the designer panel and the generated manifest stop offering it. +- **`RecordTitleChip` drops its `showStar` prop (breaking for direct users).** The chip draws its star exactly when `onToggleFavorite` is passed. A caller that passed `showStar={false}` should stop passing `onToggleFavorite` instead; a caller that relied on the star drawing without a toggle gets no star. 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 8a93caa838..648a6d0777 100644 --- a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts +++ b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts @@ -1039,7 +1039,10 @@ const UNJUDGED_SPEC_BLOCKS: Record = { * - **objectstack#6776** — `page:header` now declares `recordChrome`, * `showStar` and `showCopyId`; `page:accordion` declares `variant`; * `page:tabs` declares `tabStyle`. The five keys the renderer had read all - * along are contract now. `page:tabs` is the interesting one: the spec + * along are contract now. `showStar` has since left again: the spec + * retires it (objectstack#23650, landed as objectstack#23781), so the + * renderer stopped reading it and the registration stopped publishing it + * (objectui#12627). Its `settledUpstream` row below left with the input. `page:tabs` is the interesting one: the spec * declares BOTH `tabStyle` and `type`, so the carrier collision written up in * the deleted entry was resolved upstream by declaring the alias rather than * by renaming — which is why `page:tabs.type` stays in @@ -1344,6 +1347,24 @@ const UNPUBLISHED_EXEMPTIONS: Record = { 'object-grid.title': '@deprecated in ObjectGridSchema ("Use label instead"); GA describes it as the "Fallback for `label` (the renderer reads `label || title`)". Read as back-compat, deliberately not published — the canonical `label` IS declared. Same ruled carve-out class as the five the ruling enumerated, measured on this branch — objectui#4648 (maintainer 2026-08-16).', + // ── `page:header.showStar`: retired upstream, bump not installed yet ─────── + // objectstack#23650's ruling retires the metadata switch, and it landed as + // objectstack#23781: a `retiredKey()` tombstone on `PageHeaderProps`. The + // installed `@objectstack/spec` predates that commit and still lists the key + // as live, so the reverse direction demands it. objectui#12627 stopped the + // renderer reading it and the registration publishing it AHEAD of the bump: + // the favorites star follows the host's `onToggleFavorite` alone. + // + // ⚠️ THIS ENTRY DIES ON THE BUMP, by design, and nothing has to remember it. + // The first installed spec that carries objectstack#23781 tombstones the + // key, `specTopLevelKeys` subtracts it, and `every unpublished-key exemption + // names a key the spec really declares` and `carries no stale unpublished-key + // exemption` both name this entry. Deleting it is the whole fix. ⛔ Do not + // resolve that red by re-declaring the input or by listing this key in + // `GA_PENDING_UNPUBLISHED_KEYS`: the first publishes a key the contract + // refuses, the second would hide this entry from both checks. + 'page:header.showStar': + 'Retired upstream by objectstack#23650 (landed as objectstack#23781), which the installed spec predates. The renderer no longer reads it and the registration no longer publishes it (objectui#12627): the favorites star draws where the host wires onToggleFavorite, and metadata does not control it. Goes dangling and stale on the bump that installs the tombstone; delete it there.', // ── objectui#8176: the two lazily-registered blocks, judged for the first ── // time. ONE key left of the eighteen that entered, and as of objectui#8201 @@ -5784,7 +5805,9 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) // exemption-shaped hole. const settledUpstream: Array<[string, string]> = [ ['page:header', 'recordChrome'], - ['page:header', 'showStar'], + // `['page:header', 'showStar']` left with the input (objectui#12627): + // the spec retires the key (objectstack#23650), so neither half of + // "declared AND published" holds for it any more. ['page:header', 'showCopyId'], ['page:accordion', 'variant'], ['page:tabs', 'tabStyle'], diff --git a/content/docs/guide/layout.md b/content/docs/guide/layout.md index 39f697afc5..ccec1b615f 100644 --- a/content/docs/guide/layout.md +++ b/content/docs/guide/layout.md @@ -370,7 +370,6 @@ does not render. subtitle?: string | Record, // secondary line; same interpolation actions?: string[], // action ids, resolved against the object's own actions recordChrome?: boolean, // default true — the record chip on a record page - showStar?: boolean, // default true; drawn only where the host wires a favorites toggle showCopyId?: boolean, // default true maxVisible?: number, // positive integer; inline action buttons (renderer default 3) mobileMaxVisible?: number, // positive integer; the same on mobile (renderer default 1) @@ -379,6 +378,8 @@ does not render. } ``` +The favorites star beside the record title draws where the host wires a favorites toggle, and metadata does not control it. + > **Write `subtitle`; there is no `description` and no `icon`.** The contract refuses > `description` on this node and names `subtitle` in its place. It refuses `icon` by name as > an ADR-0087 D2 tombstone — "`page:header` property `icon` was removed in diff --git a/content/docs/layout/page-header.mdx b/content/docs/layout/page-header.mdx index 3aea91ac53..76101d4ed1 100644 --- a/content/docs/layout/page-header.mdx +++ b/content/docs/layout/page-header.mdx @@ -46,7 +46,6 @@ absent. | `subtitle` | string, or an inline locale map | — | The secondary line under the title. Same interpolation as `title`. | | `actions` | array of action ids | — | Names of actions declared on the object's own metadata, drawn as the header's button row. | | `recordChrome` | boolean | `true` (*spec*) | On a record page, renders the title as the record chip. `false` keeps the bare heading. | -| `showStar` | boolean | `true` (*spec*) | The favorite star beside the record title. Part of the record chip, and drawn only where the host wires a favorites toggle (the record context carries `onToggleFavorite`); `false` hides it even then. | | `showCopyId` | boolean | `true` (*spec*) | The copy-record-id button beside the record title. Part of the record chip. | | `maxVisible` | positive integer | `3` (*renderer*) | How many header actions render as inline buttons before the rest fold into the `⋯` menu. | | `mobileMaxVisible` | positive integer | `1` (*renderer*) | The same budget on a mobile viewport. | @@ -60,6 +59,8 @@ no `

` at all. On a record page, omit `title` and the heading is derived from There is no `description` key on this node: the contract refuses it and names `subtitle`. +The favorites star beside the record title draws where the host wires a favorites toggle, and metadata does not control it. + **`actions`** holds action ids, not nodes and not inline definitions. Each id is resolved against the object bound to the page, the same lookup `record:quick_actions` performs, so the definition lives once on the object. A header that is bound to no object resolves no ids: it diff --git a/packages/components/src/__tests__/page-header-star-host-toggle-12427.test.tsx b/packages/components/src/__tests__/page-header-star-host-toggle-12427.test.tsx index 86b3e40b94..c4066b1253 100644 --- a/packages/components/src/__tests__/page-header-star-host-toggle-12427.test.tsx +++ b/packages/components/src/__tests__/page-header-star-host-toggle-12427.test.tsx @@ -13,8 +13,8 @@ * The star is a control that stores something, so it draws only when the * `RecordContext` carries `onToggleFavorite`. Without one, the chip used to * draw a star that flipped on its own local state and stored nothing; it now - * draws none. The authored `showStar: false` still hides it when a host does - * wire a toggle. + * draws none. Metadata does not control it: an authored `showStar` changes + * nothing, because `@objectstack/spec` 18 retires the key (objectui#12627). * * Pinned through the real `SchemaRenderer` and registry, on the record-chip * layout `page:header` draws inside a `RecordContextProvider` (the harness of @@ -28,6 +28,7 @@ import { describe, it, expect, afterEach, vi } from 'vitest'; import { render, cleanup, fireEvent } from '@testing-library/react'; import { ActionProvider, RecordContextProvider, SchemaRenderer } from '@object-ui/react'; import { I18nProvider } from '@object-ui/i18n'; +import { ComponentRegistry } from '@object-ui/core'; // Registers every renderer at module scope, not in a hook // (object-ui/no-dynamic-import-in-test-hook, objectui#3010). import '../renderers'; @@ -114,11 +115,42 @@ describe('page:header record chrome: the favorite star draws only where the host ]); }); - it('the authored showStar: false hides the star even when the host wires a toggle', () => { - const { header } = renderHeader( - { isFavorite: false, onToggleFavorite: () => {} }, - { type: 'page:header', properties: { showStar: false } }, - ); - expect(stars(header)).toHaveLength(0); +}); + +describe('page:header: an authored showStar changes nothing — the host toggle is the one switch (objectui#12627)', () => { + // `@objectstack/spec` 18 retires `PageHeaderProps.showStar` + // (objectstack#23650). The renderer stopped reading it ahead of that bump, so + // the star follows `onToggleFavorite` alone in both carriers. + const AUTHORED_FALSE: Array<[string, Record]> = [ + ['on the node', { type: 'page:header', showStar: false }], + ['under properties', { type: 'page:header', properties: { showStar: false } }], + ]; + + it.each(AUTHORED_FALSE)('showStar: false %s still draws the star where the host wires a toggle', (_where, node) => { + const onToggleFavorite = vi.fn(); + const { header } = renderHeader({ isFavorite: false, onToggleFavorite }, node); + const [star, ...rest] = stars(header); + expect(rest).toHaveLength(0); + expect(star?.getAttribute('aria-label')).toBe('Add to favorites'); + fireEvent.click(star); + expect(onToggleFavorite).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['on the node', { type: 'page:header', showStar: true }], + ['under properties', { type: 'page:header', properties: { showStar: true } }], + ] as Array<[string, Record]>)( + 'showStar: true %s draws no star where the host wires no toggle', + (_where, node) => { + const { header } = renderHeader({ isFavorite: true }, node); + expect(stars(header)).toHaveLength(0); + }, + ); + + it('the page:header registration publishes no showStar input', () => { + const inputs = (ComponentRegistry.getConfig('page:header')?.inputs ?? []).map((input) => input.name); + // Non-vacuous: the sibling chip switch is still published. + expect(inputs).toContain('showCopyId'); + expect(inputs).not.toContain('showStar'); }); }); diff --git a/packages/components/src/custom/RecordTitleChip.tsx b/packages/components/src/custom/RecordTitleChip.tsx index 294a53f938..29835e284b 100644 --- a/packages/components/src/custom/RecordTitleChip.tsx +++ b/packages/components/src/custom/RecordTitleChip.tsx @@ -41,13 +41,14 @@ export interface RecordTitleChipProps { objectLabel?: string; /** Record identifier shown next to the object label with a copy button. */ resourceId?: string; - /** Hide the favourite star (default: shown). */ - showStar?: boolean; /** Hide the copy-id button (default: shown when `resourceId` is given). */ showCopyId?: boolean; /** Controlled favourite state. When omitted, the chip manages its own. */ isFavorite?: boolean; - /** Called when the favourite star is toggled. */ + /** + * Called when the favourite star is toggled. The star draws exactly when + * this is given: a star with nothing to call would store nothing. + */ onToggleFavorite?: (next: boolean) => void; /** Extra classes for the outer flex column. */ className?: string; @@ -60,7 +61,6 @@ export const RecordTitleChip: React.FC = ({ icon, objectLabel, resourceId, - showStar = true, showCopyId = true, isFavorite: isFavoriteProp, onToggleFavorite, @@ -131,7 +131,7 @@ export const RecordTitleChip: React.FC = ({ {copyLabel} )} - {showStar && ( + {onToggleFavorite && (