Repository navigation
fix(app-shell,plugin-designer): the designer's nav surfaces show a label-less entry's inherited label, the way the console draws it (objectui#11196) - #11224
Conversation
…ss entry by the runtime's inheritance rule An entry with no `label` inherits its target's current label at render time. The designer surfaces read the raw label and showed "unnamed", a positional "Item N", off-spec `title` / `name`, or a blank for exactly the entries NavigationSyncEffect now writes. - navItemLabel.ts gains navEntryLabelText / inheritedNavEntryText, which call `resolveNavItemLabel` (no second ladder), and clearedLabel, the inverse of renamedLabel; its docblock now says it reads any I18nLabel. - AppPreview, AppNavCanvas and AppNavInspector resolve an absent label through that rule with the console's own `useNavTargetLabel`. The canvas rename and the inspector Label field show inherited text as a placeholder; emptying the inspector field removes the key. - nav-selection stops reading `title` / `name`. - plugin-designer's NavigationDesigner and AppCreationWizard read labels through `resolveNavItemLabel`; it now depends on `@object-ui/layout`. Part of objectui#11196. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…y designer surface, with a runtime-parity pin - navItemLabel: the designer's text for a label-less entry equals resolveNavItemLabel's own answer for every nav type, with and without a target resolver; the control keeps an authored label verbatim; clearedLabel never writes an empty string. - AppPreview list and canvas, AppNavInspector, NavigationDesigner and the wizard's Navigation step: a label-less entry shows its inherited text, an authored label renders verbatim, and editors show inherited text as a placeholder. - Three older pins asserted "(unnamed)" / "Item N" for a truly absent label; they now assert the inherited text, and keep the fallbacks for what the rule names nothing. - Changeset for app-shell and plugin-designer. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…e, and reach the wizard's Navigation step by Next The step indicator only goes back or one step on, so the wizard pin now presses Next twice (Basic, Objects, Navigation), as the keeps-navigation pin does. The preview pin's onPatch double is typed as the prop's function type, which `tsc -p tsconfig.test.json` refused as a bare mock. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…signer-nav-label-inherit
…t, so its render settles The double minted a new client on every call. The inspector's option loader keys its effect on the client and sets state inside it, so each render re-ran the effect and the file never finished. The real hook returns a stable client; the double now does too. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…signer-nav-label-inherit
…ry by the runtime's inheritance rule The rail's NavTree rows read the raw label. A label-less leaf fell back to its target's internal name, and a label-less group heading was blank. Each row now reads `navEntryLabelText` with the console's own `useNavTargetLabel`, so it shows the target's current label, else its machine name, else the entry's id. The internal identity stays on the row's tooltip. `resolveSurface`'s Surface.label is unchanged: the canvas caption, the breadcrumb and the copilot chip read it, and those are outside this change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…that now inherit Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…ct labels to the inheritance rule The step called `resolveNavItemLabel(item)` with no target resolver, so a label-less object entry showed its objectName even though the wizard holds every object's label. The step now passes a host resolver built from the wizard's own object list. It answers an object's `label`, as the console's sidebar does, and leaves every other target to the rule's machine-name rung. `NavigationBuilderStep` is internal, so no published prop changes. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
… ObjectSelection requires A listed object that is not selected has its nav entry dropped when the Objects step is left (the wizard's deselection rule), and `selected` is a required key, which `tsc -p tsconfig.test.json` refused. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Contract reviewServed-tier: Inputs: card #11196 (body; comments ① Derived judgments1. The head carries no gate verdict, and cannot: the PR does not merge into 2. Each surface calls
3. 4. 5. plugin-designer's 6. 7. Ablations. A to D at ② Semver levelChangeset One defect in the changeset text (in the diff, and it ships in the CHANGELOG): its lines 27 to 29, "The Navigation Designer and the wizard are handed bare items and no metadata, so they show the rule's machine-name rung ( ③ Boundary flags
Findings that decide the verdict: (1) no gate ran on Implemented-by: VERDICT: FAIL Generated by Claude Code |
…11196-designer-nav-label-inherit
Conflict, one hunk in AppPreview.tsx's imports: this branch added
`type NavTargetLabelResolver` to the `@object-ui/layout` import, and
main added `import type { NavigationItemType } from '@object-ui/types'`.
Both are kept.
Semantic reconciliation:
- AppPreview.docEntry-11197 pinned a label-less `doc` entry's target text
with getByText. The entry now also reads that text as its inherited
label, so both assertions count two matches. #11197's intent (kept,
badged, target shown) holds.
- The parity pin gains `doc` entries (page, book-only, neither), and the
designer follows the runtime's merged doc rung: doc, else book, else id.
- The changeset no longer says the wizard shows the machine-name rung:
its Navigation step inherits each object's label from its own list.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Re-record after ① Derived judgments1. Gates on the head — complete, none red. 2. The merge brought only 3. The 4. The three 5–7. Unchanged. The surface-by-surface reading (①.2), the ② Semver levelUnchanged: ③ Boundary flags1–4. Open questions 1–4: the readings in Findings: none. Both findings of Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #11196
Clause-②: no
Most of objectui#11196: every designer surface this claim covers, including the Studio rail rows (after PR objectui#11202 reached
main; this branch merges it). Two sites from the card's ownsweep.SEAMlist are outside this claim's file surface and are not touched (see Remainder), so this PR does not close the card.What changed
A nav entry with no
labelinherits its target's CURRENT label at render time. The console already draws it that way (sidebar,nav:menu, command palette, search, since PR objectui#11186). The designer surfaces read the raw label instead. Every surface below now asks the runtime's own rule,resolveNavItemLabelfrom@object-ui/layout. None keeps a second ladder, andpackages/layout/src/NavigationRenderer.tsxis not edited.AppPreviewlist (normalizeNav) and its landing line(unnamed)AppNavCanvascard (navLabel)Item NAppNavInspectortitlelabel ?? title ?? name, else the positional selection idAppNavInspectorLabel field[object Object]nav-selection.tsfindNavPositionByIdlabel ?? title ?? namelabelonly; the inspector names the entry from the entry itselfStudioDesignSurface.tsxNavTreerowsNavigationDesignerrow and its live previewAppCreationWizard's Navigation stepNavTargetLabelResolverhanded to the rule)previews/navItemLabel.tsgainsnavEntryLabelTextandinheritedNavEntryText. They callresolveNavItemLabel: an authored label reads through the existingnavItemLabelTextin the designer locale, and an absent one goes to the runtime's ladder. Carrier rider (5909012131): the module docblock now saysnavItemLabelTextreads anyI18nLabel(app and action labels too). The rename is not done, because it would touchStudioDesignSurface.tsxandObjectActionsPanel.tsx, which are outside this claim.useNavTargetLabel. Measured: it resolves in the console's i18next language, and the designer locale (useMetadataLocale) is that same language collapsed toen-US/zh-CN. So a label-less entry reads in the designer exactly as the console draws it. A designer-locale twin would have needed either a second metadata resolver or an edit touseNavTargetLabel.ts, which is outside this claim. Limit: in a session of one of the eight other shipped locales, inherited text follows the console language, while the designer resolves an authored map label in English.objectName,dashboardName,pageName, ...), else the entry'sid. A target the host cannot read (a draft, an unsaved page, no metadata loaded) falls to the machine name, as it does in the console before its metadata loads.engine.appPreview.unnamedand the positionalengine.appNav.itemrow stay only for what the rule names nothing: a separator, a label that is present but resolves to nothing (an empty map), and an entry with no target and noid. No i18n string is added or removed.renamedLabel. Emptying the inspector field restores inheritance: the newclearedLabel(the inverse ofrenamedLabel) answers what is left, and when nothing is left thelabelkey is removed, never written as''.@object-ui/layout. There is no cycle: layout depends on components, core, react and types, and none of them depends on plugin-designer. Bundle: plugin-designer's vite build externalizes every bare specifier. Measured on the builtdist/index.js: oneimport ... from "@object-ui/layout"and 0 inlined definitions of the ladder. For a consumer, layout is side-effectful and brings@dnd-kit, but the console already ships it. The Navigation Designer is handed bare items and no metadata, so it shows the rule's machine-name rung. The app wizard's Navigation step hands the rule a host resolver built from its own object list, so a label-less object entry shows the object's label there. For pages (what the console's navigation sync writes) that is exactly what the console shows. A label-less dashboard showsdashboardNamewhere the console shows its label. The seat has the options for that.Remainder (not in this PR)
resolveSurface'sSurface.label(navSurface.ts). It is unchanged: it still carries''for a label-less entry. Its readers are the canvas caption, the breadcrumb and the copilot chip, all inStudioDesignSurface.tsxoutside theNavTreerows this claim allows. The chip'stype · namefallback for an unlabelled leaf is pinned byStudioDesignSurface.aiChipLabel.test.tsx(objectui#8219 item 3). Making them inherit is a ruling for the seat, not a line change.StudioNavItemInspector(inStudioDesignSurface.tsx). Its label reads are in the card'ssweep.SEAMlist, but outside this claim's file surface ("NavTreerows only"). Two gaps remain there. Clearing its Label field writes''throughrenamedLabel. Picking an object for a label-less entry stores the object's label, where the entry would otherwise keep inheriting it.AppNavCanvas'saddItembirth label. A new entry is born with the localized "New item" label, andAppNavInspector's object picker keeps it, so the entry never inherits.The seat answered the dev's open questions (
5914452475gives the review's readings):resolveSurfaceinherits (A, with the objectui#8219 chip pin re-judged);StudioNavItemInspectorfollowsAppNavInspector's shape (A);addItembirths a label-less entry (A, with the objectui#11189 autosave pins re-judged). All three are this card's next slice, after PR objectui#11225 lands, because they share itsStudioDesignSurface.tsxhunks.NavigationDesignerstays on the machine-name rung until a host with metadata appears.Tests and gates
Measured at
2a6f0c5fb, after mergingorigin/main(it carries PR objectui#11202 and objectui#11206's fix). Later commits:693024cc2(the changeset),d78e28afdanddfbf137fa(the wizard step's host resolver and its pin), and6e8f1d83f(a merge ofmaincarrying PR objectui#11207 / objectui#11197). The re-runs at6e8f1d83fare the group "At the merged head" below. Build, type-check and test runs went through the shared verify lock, from the worktree root; thecheck:*gates and eslint are lock-free.turbo run build --filter='@object-ui/app-shell^...' --concurrency=2, exit 0.pnpm --filter @object-ui/plugin-designer type-checkexit 0;pnpm --filter @object-ui/app-shell type-checkexit 0 (bothtsc --noEmitandtsc -p tsconfig.test.json).pnpm exec vitest run, every run undertimeout 300, with--testTimeout=20000 --reporter=verbose:packages/plugin-designer/: 28 files / 203 tests passed.packages/app-shell/src/views/studio-design/: 77 files / 434 tests passed.packages/app-shell/src/views/metadata-admin/in eight chunks: 358 files / 4228 tests passed, 0 failed.previews/navItemLabel.inherit-11196.test.ts: the parity pin. For every nav type, with and without a resolver, the designer's text for a label-less entry equalsresolveNavItemLabel's own answer.previews/AppPreview.labelInherits-11196.test.tsxcovers the list and the canvas.inspectors/AppNavInspector.labelInherits-11196.test.tsxcovers the title, placeholder, typing, and clearing (which removes the key).studio-design/StudioDesignSurface.railLabelInherits-11196.test.tsxcovers the rail rows.__tests__/NavigationDesigner.labelInherits-11196.test.tsxcovers the rows, the live preview, the inline placeholder and the wizard step.(unnamed)/Item Nfor a truly absent label:AppPreview.labelMaps-11100,AppNavCanvas.labelMaps-11128andAppPreview.test.tsx. They now assert the inherited text, and keep the fallbacks for what the rule names nothing.ablation-replace.mjsin wrap mode. Each mutation was proven to land on disk, and each restore was proven against HEAD (blob equals HEAD, andgit diff HEADis empty). The pins importsrcdirectly, so there is nodistleg.AppPreviewreads the raw label: 3 of 6 red, the three list cases.NavigationDesignerlive preview readsresolveKeyedI18nLabel(item.label): 1 of 4 red.NavTreereads the raw label: 2 of 3 red.Contactassertion), atdfbf137fa.pnpm exec eslinton the changed files: 0 errors. Warnings per source file are identical in count and rule set to the base versions, measured by lintinggit show BASE:pathwith--stdin-filename.693024cc2:check-changeset-presence,check-control-bytes,check:new-line-citations(0 new),check:phantom-deps,check:unused-deps,check:changeset-claims,check:pending-changeset-literals,changeset:check, andmarkdown-test-inputs.mjs --audit,check:self-importandcheck:pre-install-import-graph.scripts/__tests__/(owed because this PR edits a markdown file, the changeset): NOT MEASURED when this PR was opened, because the verify-lock queue timed out twice. The reading is in theos-dev-reportcomment on the card.check:changeset-claimsasked for a read of four pending changesets that name files this PR edits: threepnpm-lock.yamlparagraphs about spec resolution, and6681about marketplace chunks. All four are still true; this PR only adds plugin-designer's workspace link to@object-ui/layoutand one hook import.pnpm lintand the e2e suites. The page is not browser-verified. The pins render the real components under a realMetadataCtx.Spec Main Shape Gatered of objectui#11206 is fixed onmain, and this branch carries that fix.6e8f1d83f(under the lock): the closure build, exit 0;type-checkfor app-shell and plugin-designer, exit 0 each; vitest overmetadata-admin/previews/,metadata-admin/inspectors/andstudio-design/in 6 chunks, 284 files / 3203 tests passed;packages/plugin-designer/, 28 files / 203 tests passed. Lock-free, exit 0:markdown-test-inputs --audit,check-changeset-presence,changeset:check,check:changeset-claims,check:pending-changeset-literals,check-control-bytes,check:new-line-citations(0 new),check:phantom-deps,check:unused-deps.Acceptance notes
AppNavInspector'sensureNavIdbuilds an id fromString(node.objectName ?? node.label ?? 'item'). For an entry with noidand noobjectNamewhose label is a locale map, that reads[object Object]and would yieldnav_object_object. This is a read-only inference, not reproduced; the spec requires every nav entry to carry anid. Not in scope here.objectui#11197 landed as PR objectui#11207 (
e6bc087a3), which also editedAppPreview.tsx,AppNavCanvas.tsxandNavigationDesigner.tsx. This branch mergedmainat6e8f1d83f:AppPreview.tsximports, where both lines are kept;AppPreview.docEntry-11197pin now finds a label-lessdocentry's text twice (its target, and its inherited label), so its two assertions usegetAllByText. The entry is still kept, badgeddoc, with its target shown.The parity pin gains three label-less
docentries (with a page, with only a book, with neither). For each, the designer answers exactly what the runtime answers: the page, else the book, else theid.Session:
https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec(domain:ui seat 2, dispatched dev for objectui#11196).Generated by Claude Code