Skip to content

fix(layout): page-header 的 registration inputs 不再宣告 description - #3265

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-3226-page-header-inputs
Aug 3, 2026
Merged

fix(layout): page-header 的 registration inputs 不再宣告 description#3265
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-3226-page-header-inputs

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #3226

⚠️ 本 PR 只做原 issue 的一半(objectui 本地那一半)

按 issue 上两条 PM 裁定(#3226 的 comment):本单走路线 B(ADR-0087 D2 conversion 条目)而非直接删除,而 conversion 机制在 objectstack 仓,不在这里。因此本 PR 的范围被明确收窄为无条件成立、且跨仓依赖为零的那一半:

原 issue 原文:「两种路线都要求 inputs 不再宣告 description —— 这一点无论哪条路都成立。」

本 PR 做的:registerLayout()page-headerinputs[title, description] 收窄为 [title, subtitle]

本 PR 刻意不做的:不删 PageHeader.tsxsubtitle ?? description,不删 description prop。这两项被本 PR 的测试主动钉住了(见下),删它们等于走回被否决的删除路线。

剩余部分(阻塞在上游):conversion 条目 page-header-subtitle-alias(descriptionsubtitle,加载时改写成 canonical 键)+ 随后删除消费端裸 ??description prop。这部分归主 backlog PM,在 objectstack 立单;本单挂 Blocked-by


为什么这一半可以现在就做,而且必须现在做

inputs声明面,不是文档。设计器用它生成属性面板,framework 的 check:react-declaration-parity 拿它跟 spec schema 对差。所以宣告 description 不是「容忍一个遗留拼写」,而是对外发布了第二套方言,并且在作者(尤其是读 registry 学形状的 AI 作者)写一个 spec 里根本不存在的键:

  • @objectstack/spec/uiPageHeaderProps 的 shape 是 title / subtitle / icon / breadcrumb / actions / aria —— 没有 description(已实测)。
  • canonical 的 page:header(@object-ui/components containers.tsx)声明的是 subtitle
  • 于是同一份 metadata:写 descriptionpage-header 下渲染出副标题,换到 page:header静默丢失。同一份 JSON,两个结果 —— 这正是「单一契约」要消灭的东西。

收窄声明面不改变任何运行时行为(?? 还在),因此对仓外消费者零破坏;而把声明面留在错的状态,只会持续生产出将来 conversion 层还得去吸收的错误 metadata。

为什么运行时的 ?? 现在不能删

这个别名存在的全部理由就是仓外的消费者 schema(佐证:registerLayout() 在本仓没有任何调用点,它纯粹是 @object-ui/layout 对外的公开导出)。所以「仓内 grep 零命中」(我复核过,确实为零)不构成「没人在写」的证据。按删除路线走,外部写 description 的页面会静默丢副标题 —— 标题照常渲染,只是第二行没了,是最难被报障的失效形态。

预警的门禁冲突:核实过,不存在

PM 预警了 check:react-declaration-parity 可能校验「声明的 inputs ↔ 组件实际读取的 props」。查了 framework 侧实现,不会冲突,两个独立理由:

  1. 它比对的是两份声明(spec zod props vs registry inputs),其文件头明确写着「It never looks at a renderer」。组件读不读 description 完全不在它视野内。
  2. 它的 ratchet 只对 NEW registry-only inputs(registry 声明了而 spec 没有的)报警。description 恰恰就是一个 registry-only input,删掉它只会减少 divergence,不可能触发 ratchet
  3. 另外,它的覆盖面是 REACT_BLOCKS(ObjectForm / ListView / ObjectChart / Block),baseline 里也只有这三个 block;page-header 根本不在其中。

测试:两半都钉住

新增 packages/layout/src/__tests__/page-header-authorable-keys.test.tsx(7 个用例):

  • 声明面:page-headerlayout:page-header 两个键都不得description;必须subtitle;并且 —— 这条是从 spec 自己的 shape 推出来的,不是手写白名单 —— 声明的每一个 input 都必须是 PageHeaderProps 的键。将来再往这里加一个 spec 没有的 input,会因为和 description 完全相同的理由变红,不需要谁记得这个 issue。
  • 顺序守卫:运行时仍把 legacy description 渲染成副标题、且 subtitle 在两者都在时胜出 —— 这不是为别名背书,而是钉住顺序:在 conversion 条目落地前删掉这个读取,就是被否决的删除路线。注释里写明:conversion 落地时,同一个 commit 里删掉 fallback 和这两个用例。

验证

pnpm exec vitest run --maxWorkers=2 packages/layout
  Test Files  6 passed (6)
       Tests  78 passed (78)

pnpm exec turbo run type-check --filter=@object-ui/layout --concurrency=2
  Tasks:    9 successful, 9 total

pnpm --filter @object-ui/layout lint     → 0 errors(42 warnings,全部为既有)
pnpm run check:spec-symbols              → ✅ 1192 files scanned
pnpm run changeset:check                 → ✅ fixed group ok / no `major`

Changeset

有 —— patch(@object-ui/layout)。虽然运行时行为不变,但声明面是对外发布的:设计器属性面板会从 "Description" 变成 "Subtitle",生成的 sdui.manifest.json 也随之改变,对使用设计器的人是可见的。按 AGENTS.md 未标 major


顺带发现(未在本 PR 修,已另开 issue)

文档里同样在教 description,而且比 inputs 教得更直接 —— 但它在 content/docs/,超出本单的 packages/layout 范围,且牵扯到别的 doc drift(breadcrumbs 这个 prop 组件根本不读),不适合搭车:

  • content/docs/guide/layout.md「PageHeader Component」的 authored JSON 示例写 "type": "page-header" + "description",Schema API 块声明 description?: string
  • content/docs/layout/page-header.mdx「Component Props」块列 description?: string,且完全没提 subtitle

https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa


Generated by Claude Code

…s an input

`registerLayout()` declared `inputs: [title, description]` for the legacy
kebab alias, while `@objectstack/spec/ui`'s `PageHeaderProps` — and the
canonical `page:header` renderer in `@object-ui/components` — declare
`subtitle`. `inputs` is a DECLARATION surface (the designer palette, the
framework's `check:react-declaration-parity`), so this did not tolerate a
legacy spelling, it published a second dialect and taught authors a
non-spec key. Metadata that took the offer renders a subtitle under
`page-header` and loses it under `page:header`.

The runtime `subtitle ?? description` read in `PageHeader.tsx` stays on
purpose: the alias exists for out-of-repo consumer schemas, so zero in-repo
hits is not evidence nobody writes it, and dropping the read would silently
delete an external page's second line. It is retired with the ADR-0087 D2
conversion entry `page-header-subtitle-alias`, which lives in the framework
repo. Narrowing the declaration is unconditional and changes no runtime
behaviour.

New tests pin both halves: the registration may not declare `description`,
must declare `subtitle`, and may declare nothing the spec's own shape does
not; the runtime fallback is pinned as a sequencing guard, to be deleted
together with the conversion entry.

Refs #3226

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 3, 2026 9:05am

Request Review

@github-actions github-actions Bot added the tests label Aug 3, 2026
@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 09:06
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit d2363e7 Aug 3, 2026
17 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-3226-page-header-inputs branch August 3, 2026 09:07
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CfHWHxqB.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 476.24KB 104.56KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 223.53KB 54.78KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 37.96KB 10.54KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 60.54KB 17.13KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.01KB 28.86KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 230.54KB 56.80KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.49KB 26.95KB
plugin-gantt (index.js) 162.25KB 39.55KB
plugin-grid (index.js) 185.08KB 49.04KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 104.94KB 25.32KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.55KB 10.59KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.67KB 20.43KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

page-headerpage:header 对外声明了两套 authorable 键:description vs subtitle,消费端用 ?? 兜底

2 participants