Skip to content

fix(components): resolve ui:breadcrumb and ui:command child item icons through the record resolver - #6644

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-5931-child-icon-resolution
Aug 28, 2026
Merged

fix(components): resolve ui:breadcrumb and ui:command child item icons through the record resolver#6644
os-sales merged 2 commits into
mainfrom
claude/issue-5931-child-icon-resolution

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Part of #5931 — deliberately not a closing keyword. Two of the card's three components are wired here; the third, ui:button-group, is routed for a decision below and is untouched by this branch. Merging this should not close the card.

Mirrors the pattern PR #5994 landed for ui:dropdown-menu and PR #6324 landed for ui:context-menu, rather than re-deriving one.

What was wrong

BreadcrumbItem.icon and CommandItem.icon are declared, published keys of the protocol — present in @object-ui/types, mirrored in the Zod schemas, and documented on both components' pages — and neither renderer contained the substring icon at all. The catalog fixture literally named components-data-display-breadcrumb/with-icons rendered no icons; the nine names the two command fixtures declare all drew nothing.

What changed

Fixture spellings — two were already dead

A fixture must not declare a name that draws nothing. Every name the three components' fixtures declare was checked against lucide's runtime icons record (lucide-react 1.31.0, 1767 keys). Two were retired spellings that are absent from the record, corrected to the live key the retired export resolves to by identity:

fixture was now identity
components-data-display-breadcrumb/with-icons layout panels-top-left Layout === PanelsTopLeft
components-form-command/command-menu smile face-slightly-smiling Smile === FaceSlightlySmiling

The other ten names (home, book, calendar, calculator, user, credit-card, settings, file-plus, folder-open, save) are live. home is the one whose glyph class is not its own spelling — resolveIcon's iconNameMap sends it to lucide's House, and the fixture test asserts lucide-house so that indirection is pinned from the outside.

⚠️ ui:button-group is NOT wired here — it needs a decision

Its three fixtures author icon on buttons[] and its docs page documents icon?: string on ButtonGroupButton, but ButtonGroupButton declares no icon key — not in packages/types/src/navigation.ts and not in its Zod mirror packages/types/src/zod/navigation.zod.ts. Wiring it therefore means first widening the published item contract, which is the card's "stop and report, do not choose" branch. Measured, not inferred: rendering components-basic-button-group/icon-toolbar.json through the registry today yields 3 buttons, all with empty textContent, 0 svg — the "Toolbar Actions" example on the published docs page is three blank buttons. So stripping the key is not the honest branch either; it would empty that fixture. Full options and a recommendation are in the developer report on #5931.

The same divergence covers value, selectionMode, onValueChange and disabled, all documented on that page and all absent from the type — which is why this is one decision, not four.

The lucide gate's census row for button-group is updated to say this explicitly, so the next reader does not have to re-derive why it is the one container in that table still left undeclared.

Gate census (scripts/check-lucide-icon-record-names.mjs)

Extended the same way #6324 extended it when context-menu's repair landed: a census entry declares that a type's names now reach a vocabulary, and it could not have been added before the repair. breadcrumb and command are declared with descendants: true and a measured min (3 and 9) rather than the weaker min: 1 its siblings carry — a descent that reached one of nine would be as blind as one that reached none. command's names sit at groups[].items[].icon, a depth the single-level ARRAY_PATH cannot express at all, so descent is the only non-vacuous form available. Judged names: 167 → 179 (+12), exactly the measured population.

⛔ This does not teach the gate to guess at untyped child nodes; that blind spot stays filed as #5992 and is untouched. The gate's stale header rows for breadcrumb, command and context-menu (the last falsified by #6324) are corrected, and two comments in its own test suite that cited breadcrumb/command as examples of containers reaching no resolver now cite button-group, which still is one.

Verification — at 1352da1f

  • New suites packages/components/src/__tests__/breadcrumb-item-icon.test.tsx and command-item-icon.test.tsx — 20 tests, green. Renderers invoked via ComponentRegistry.get(name), not SchemaRenderer (fix(components): resolve ui:dropdown-menu item icons instead of rendering the name as text #5994's stated reason).
  • Both directions asserted per component. The discriminating assertion is the resolved glyph, named as svg.lucide-KEY; the "authored name is not on screen as text" direction is asserted once each and labelled honestly as a regression guard, not a discriminator — unlike dropdown-menu, these renderers never printed the name, so queryByText is null in both worlds.
  • The instrument carries a positive control in the same query shape: BreadcrumbSeparator's chevron and CommandInput's search glyph are asserted present at container level while the item-scoped query is red pre-fix, so a red row cannot be misread as a broken query. Each row scopes to the crumb's own li / the item's own cmdk element, so neither sibling glyph leaks in.
  • Lazy mount checked, and the answer recorded as a measurement: neither component defers. Breadcrumb is plain nav/ol/li markup with no Radix root; ui:command renders cmdk inline (CommandDialog is a separate export this renderer does not use). A harness control asserts the items really mounted, so no "renders no glyph" row can pass vacuously.
  • Ablation — reverted both renderers to origin/main from a committed state, confirmed the mutation on disk (0 resolveIcon occurrences, blob hash differs from the HEAD blob), re-ran: 8 failed / 12 passed. The 8 are exactly the glyph-presence rows across both breadcrumb arms and command's arm plus the fixture rows; the 12 that stay green are the harness controls and the retired/unknown/no-icon rows, which are correctly null in both worlds. Restored and proved the restore: git diff HEAD empty and the on-disk hash equal to the HEAD blob.
  • Gate reverse-verified: putting layout and smile back turns check:icon-record-names red on both, naming the exact JSON pointers — and the gate independently derived the same two replacements this PR chose. Restored and re-proved.
  • Anchored grep with a positive control in one query shape: {item|button|crumb.icon} as a bare JSX text render — 0 hits across all three renderer files, while the same query shape finds the label renders in two of them (breadcrumb's third is resolveKeyedI18nLabel(item.label), shown separately).
  • Suites run from the repo root, the canonical form: packages/components/ 199 files / 1810 tests green; examples/schema-catalog/ + the gate's own suite 15 files / 1874 tests green; the six adjacent suites naming these types 6 files / 70 tests green.
  • type-check green, and proved to be a real measurement — tsc --listFiles shows both edited renderers and both new test files inside the compiled programs.
  • eslint . over the repo: 3875 files, 0 errors. The touched files carry 0 errors and only pre-existing warning classes.
  • Gates green at 1352da1f: check:icon-record-names, check:control-bytes, check:doc-types, check:doc-fences, check:doc-snippets (after building its prerequisites), check-changeset-presence, check-changeset-no-major.

The changeset scores patch for @object-ui/components; the presence gate ruled it owed and it was not pre-scored in either direction.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3237.3 KB 3266.6 KB
Main entry chunk (gzip) 157.3 KB 350 KB
Entry file index-DKh23nLV.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) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
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) 507.87KB 115.07KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 238.89KB 60.02KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
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.55KB 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.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.85KB 12.89KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.41KB 34.47KB
plugin-designer (index.js) 212.80KB 43.15KB
plugin-detail (index.js) 245.29KB 62.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.01KB 32.23KB
plugin-gantt (index.js) 165.16KB 40.33KB
plugin-grid (index.js) 201.66KB 54.57KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 112.86KB 27.54KB
plugin-map (index.js) 20.09KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.72KB 7.71KB
plugin-tree (index.js) 9.26KB 3.13KB
plugin-view (index.js) 85.87KB 21.12KB
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) 65.97KB 21.98KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 9.30KB 3.22KB
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) 2.74KB 1.41KB
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 (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.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants