Skip to content

feat(plugin-detail): declare icon on the detail-section node (objectui#9529) - #10484

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-9529-detail-section-declares-icon
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-9529-detail-section-declares-icon

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #9529
Clause-②: yes

Declares icon on the detail-section node, per the director ruling on the card (comment 5724939941, letter 1): { name: 'icon', type: 'string' } with a one-line description on the registration in packages/plugin-detail/src/index.tsx, and 'icon' in DETAIL_SECTION_NODE_INPUTS. DetailSection already draws section.icon in both header branches; the node now declares the key, and its adapter folds it through.

What changed

File Change
packages/plugin-detail/src/index.tsx the detail-section registration gains { name: 'icon', type: 'string', description }; two comment phrases counting "the eight declared inputs" made count-free
packages/plugin-detail/src/DetailSectionNode.tsx DETAIL_SECTION_NODE_INPUTS gains 'icon', so the adapter folds it into section; present-tense "eight" claims in its comments made count-free (the #8626 history keeps its past-tense count)
packages/plugin-detail/src/__tests__/detailSectionAuthoredIcon-9529.test.tsx the new pin, five rows
.changeset/9529-detail-section-declares-icon.md minor on @object-ui/plugin-detail; states the one widened key and carries the Clause-② line

Two edits, not one source — measured, not chosen. The registration RESTATES the list rather than spreading the constant: each registration entry carries a type, required, an enum or a description, while DETAIL_SECTION_NODE_INPUTS is the name list the fold iterates. The two are already held together by the fold-parity row of detailSectionAuthoredNode-8626.test.tsx, which compares them in both directions and goes red under both ablation legs below. Deriving one from the other is a refactor this card does not carry.

Premises, measured on base 274e14af4 before the edit

  1. DETAIL_SECTION_NODE_INPUTS lives in DetailSectionNode.tsx; the registration restates it (above). Held.
  2. DetailSection.tsx reads section.icon twice, once per header branch (the non-collapsible Card header and the collapsible trigger header). Measured on the NODE path: the pin renders through the real SchemaRenderer and the real registry, as the finding(plugin-detail): every detail-section input is a FLAT prop the renderer never reads — DetailSection reads section.* only, and an authored node hands it section === undefined #8626 pin does. Held.
  3. The validator warns on an authored icon today: a probe against a manifest built the way page.tsx builds it returned warning unknown-prop for icon. Held for icon; falsified for the proposed control. visible draws NOTHING on base or head: it is in the validator's BASE_PROPS (sdui-parser's validate.ts), the keys every node may carry, which are never judged against inputs. The pin's lit control is name instead: warning unknown-prop on both sides.
  4. Regeneration: none of sdui.manifest.json, sdui-intrinsics.d.ts, sdui-blocks.md is checked in (git ls-files finds none of the three; control: the same listing finds gen-manifest.ts). And detail-section is outside the public tier: the probe read getPublicConfigs() without it (false, while record:details reads true), so gen-manifest.ts would not emit it even if run. Nothing to regenerate.
  5. Text readers: no checked-in test outside plugin-detail names detail-section. The four console readers the dispatch named have 0 hits each (record:details is the lit control in three of them), and they stay green. The in-package readers that do name it, the finding(plugin-detail): every detail-section input is a FLAT prop the renderer never reads — DetailSection reads section.* only, and an authored node hands it section === undefined #8626 pin (fold parity, both directions) and the detail-section still offers headerColor as a free-form string input, so the SDUI authoring surface invites values the published validator now refuses #6955 pin, stay green unmodified. None was weakened.
  6. name and visible stay undeclared.

The pin: detailSectionAuthoredIcon-9529.test.tsx

Row Base 274e14af4 Head
draws no diagnostic for an authored string icon red: unknown-prop green
judges the authored icon as a declared STRING input (icon: 42 draws type-mismatch naming icon) red: unknown-prop green
CONTROL: name still draws unknown-prop green green
draws the authored Lucide icon before the title (collapsible: false); the same node without icon draws nothing there red green
the same, collapsible: true red green

Base: Tests 4 failed | 1 passed (5). Head: Tests 5 passed (5).

The render verdict is the GLYPH: the drawn svg's markup must equal MapPin's. LazyIcon draws the Database glyph while it loads and for an unknown name, so a row that only asked "is there an svg" would pass on the fallback.

Ablation: two legs, each proven on disk and restored by blob

Mutations went through node ../objectstack/scripts/ablation-replace.mjs --delete (the anchor must hit exactly once, the blob must change; restore is git checkout HEAD -- on an absolute path, verified by blob equality with HEAD and an empty git diff HEAD), inside a wrapper carrying its own trap … EXIT INT TERM that repeats that verification. Both legs ran on the committed implementation. No build sits between mutation and assertion: the pin imports ../index relatively, and the root vitest config aliases the @object-ui/* packages it imports (core, react, sdui-parser) to their src.

Leg Mutation Landed Red Stayed green Restored
A (the dispatch's) delete the icon entry from the registration's inputs anchor 1 to 0, blob f9a01c588 to 10eddd5d7 both validate rows, and #8626 fold parity both render rows, the name control, the other six #8626 rows blob f9a01c588 equals HEAD, diff empty
B delete 'icon' from DETAIL_SECTION_NODE_INPUTS anchor 1 to 0, blob 2612f9a8c to 3e0dd79e3 both render rows, and #8626 fold parity both validate rows, the control, the other six #8626 rows blob 2612f9a8c equals HEAD, diff empty

Each leg read Tests 3 failed | 9 passed (12) over the #9529 and #8626 pins. Each row reddens under the mutation of the thing it names, and only under that one.

Verification, at the final head 6b100e89a

What Command Result
package + text readers pnpm exec vitest run packages/plugin-detail/ plus the four console readers (registry-inputs-spec-parity, ga-honoured-inputs-author-reach, public-contract, component-input-union-specimens), from the repo root Test Files 208 passed / 1 skipped (209), Tests 2270 passed / 8 skipped (2278)
dependency closure pnpm exec turbo run build --filter='@object-ui/plugin-detail^...' --concurrency=2 Tasks: 11 successful, 11 total (run at 37d66e3de; the only later commit edits comments in the new test file)
types pnpm --filter @object-ui/plugin-detail type-check exit 0; tsc -p tsconfig.test.json --listFiles lists the new pin (1 hit)
control bytes pnpm check:control-bytes OK (scanned 8544 tracked text file(s); skipped 85 binary)
line citations pnpm check:new-line-citations 0 new citation(s), enforcement report-only -> exit 0
changeset node scripts/check-changeset-presence.mjs 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
no major node scripts/check-changeset-no-major.mjs No changeset declares a major bump
changeset claims pnpm check:changeset-claims report-only, exit 0. Two pending changesets name packages/plugin-detail/src/index.tsx: 9280-record-highlights-entry-icon-retired.md (the record:highlights fields description) and record-alert-cta-label-i18n-4998.md (record:alert's action). Both paragraphs read; both still true, since this diff touches neither registration
gates reading these files check:doc-types, check:registry-bare-names, check:icon-record-names, check:test-path-roots, check:pending-changeset-literals all exit 0
component-surface parity (report-only) pnpm check:component-surface-parity exit 0; lists detail-section.icon as input-outside-keyset beside seven of the pre-existing detail-section inputs, for their reason: the gate sees no named read through DetailSectionNode, which folds by iterating the list
NOT MEASURED pnpm check:sdui-registration-pins exit 2, PREREQUISITE NOT MET (it needs a console build); left to CI

Lint is narrowed, and the narrowing is declared. eslint --no-inline-config --format json over the three touched source files: 3 files linted, 0 errors. Per-rule counts on the two edited files equal base (base read with git show 274e14af4:PATH piped to eslint --stdin --stdin-filename PATH): DetailSectionNode.tsx 1 react-refresh/only-export-components; index.tsx 29 react-refresh/only-export-components and 2 @typescript-eslint/no-explicit-any. The new pin: 0 messages. The narrowing is a measurement because type-aware linting is not enabled: eslint.config.js extends tseslint.configs.recommended and declares no parserOptions.project or projectService, so this diff cannot move the verdict on any file it does not contain. The package's own eslint . resolves the same root config (there is no package-level one). The repo-wide run is CI's.

Acceptance notes

  • For the seat: hideEmpty is now a second member of this card's class. The card's "exactly ONE member" was measured at bbc9dc3, where DetailSection had no live section.hideEmpty read and DetailViewSection had no hideEmpty. objectui#8603 has since restored both. At base 274e14af4, DetailSection reads section.hideEmpty === true (the all-empty hide), the node does not declare hideEmpty, an authored one draws unknown-prop (probe), and the adapter does not fold it. Not touched here: the ruling names icon only, and a widening is a product decision, the more so for a key whose omitted default differs between record:details and detail-view (the DetailViewSection.hideEmpty doc block records that split). Handed to the seat in the report.
  • One supporting fact in the ruling points at a different call site. The ruling says DetailView.tsx writes icon itself in three places through direct DetailSection use; the three icon: literals there are ACTION icons (share-2, square-pen, trash-2). A direct-path section icon does exist, through buildDefaultPageSchema, which passes a field group's icon to its section descriptors. The ruling's outcome is unaffected, and nothing in this PR depends on it.
  • A pending changeset describes the old count. .changeset/8626-detail-section-authored-node.md (unreleased) says in present tense that the registration "declares eight FLAT inputs"; it will publish in the same release as this PR's changeset, which states the widening. Not edited: it is outside this claim's file surface. Whether it wants a prose correction is the seat's call.
  • packages/plugin-detail/README.md documents no input list for detail-section (its one node example sits inside tabs[].content and writes fields), so no doc line became false. None added, to stay inside the claimed file surface.

Dispatched by the domain:ui seat #1; session https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC.


Generated by Claude Code

…i#9529)

DetailSection draws section.icon in both header branches, but the
detail-section registration did not declare icon among its inputs and the
node adapter folded only declared names, so an authored icon drew an
unknown-prop warning from the JSX-page validator and rendered nothing.

Declare { name: 'icon', type: 'string' } on the registration and in
DETAIL_SECTION_NODE_INPUTS, so the adapter folds it. The new pin reads the
validator verdict, the declared type, and the drawn glyph in both header
branches, with name as the lit unknown-prop control.

Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Name the AGENTS.md test-discipline section in English and state only the
module relationship that was read off lucide's ESM entry.

Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/9280-record-highlights-entry-icon-retired.md

  • names packages/plugin-detail/src/index.tsx → packages/plugin-detail/src/index.tsx — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.

.changeset/record-alert-cta-label-i18n-4998.md

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 2a943bf0c (merge-base with origin/main): 3 file(s) changed outside .changeset/, read against 1407 pending declaration(s) that publish a body (1986 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3045.2 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-CdSEOFum.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
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) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
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) 544.59KB 130.15KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 256.92KB 65.08KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.22KB 14.56KB
plugin-charts (index.js) 74.76KB 20.82KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.59KB 35.39KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 231.81KB 61.27KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 168.25KB 41.46KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.37KB 28.24KB
plugin-map (index.js) 21.83KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.90KB 21.97KB
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.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 04:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 80a0ecd Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-9529-detail-section-declares-icon branch September 25, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant