Skip to content

fix(components): resolve ui:dropdown-menu item icons instead of rendering the name as text - #5994

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5930-dropdown-menu-icon-resolution
Aug 24, 2026
Merged

fix(components): resolve ui:dropdown-menu item icons instead of rendering the name as text#5994
yinlianghui merged 2 commits into
mainfrom
claude/issue-5930-dropdown-menu-icon-resolution

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5930

Both arms of renderMenuItems in packages/components/src/renderers/overlay/dropdown-menu.tsx rendered a menu item's authored iconstring straight into a text node, so the catalog fixture literally named with-icons.json drew edit Edit, copy Copy, trash Delete.

Both arms now resolve the name through resolveIcon.

Route taken

Per the dispatch ruling, the record surface (resolveIcon from renderers/action/resolve-icon.ts) — what action:* and ui:button next door already resolve against. A retired or unknown spelling renders nothing. The dynamic surface (LazyIcon) was not used: it degrades an unknown name to the Database glyph, trading a no-icon failure for a wrong-icon one, recorded as ruled out for authored icon fields by #5622 and #5633. Retiring the key was likewise not taken.

Premise verified before any edit.resolveIcon lives in the same package, one directory over; renderers/complex/data-table.tsx:12 already imports it as '../action/resolve-icon', so this is an established sibling import, no new cross-package dependency and no layering inversion.

One nuance worth recording: ui:button is the same resolver by behaviour, not by identity — renderers/form/button.tsx carries a byte-equivalent inlined copy (its own toPascalCase, its own Home -> House map, its own index into icons) rather than importing the shared function. Same population, same casing, same outcome, so the ruling holds exactly. Filed as #5993.

Line numbers re-derived

The card's ~40 / ~52 were days old. Re-derived from the tree: exactly 40 and 52, with a control probe (a deliberately nonsensical item.iconXYZZY returned exit 1) so a zero-hit would have read as real rather than as a silent miss.

The fixture carried a RETIRED spelling

edit is not a live key of lucide's runtime icons record (measured against the installed lucide-react 1.31.0: 1767 keys, Edit absent, Copy and Trash present). It survives only as a deprecated export. So after this fix the first item of with-icons.json would have drawn no glyph at all — a fixture named for its icons, silently showing two of three.

Corrected to square-pen, derived by identity rather than by preference — the method the gate itself documents for naming a replacement:

identity-derived live key(s) for Edit: ["SquarePen"]

This matches the repo's own established repair: packages/plugin-detail/src/DetailView.tsx:792 already spells this same glyph icon: 'square-pen', from #5622's fix of the same retired name.

⚠️Flagging this as the one judgment call. The dispatch said a non-live name is "a finding worth reporting, not a reason to change the fixture to easier names", while also requiring that the fixture "must actually render glyphs" after the fix. This is read as correcting a dead spelling to its identity-equivalent live one — the documented repair, not an easier name — but it is a one-line change and trivially reversible if the maintainer reads it the other way.

Does this make the #5633 gate newly relevant here?

No — and that is itself a finding. Measured both directions:

  • Census unchanged. Part 1 rediscovers modules reading the icons record directly. This PR imports resolveIcon instead, so dropdown-menu.tsx correctly does not enter DECLARED_RECORD_READERS; the gate still reports 8 record-reading resolvers.
  • The names still are not judged. Part 2 keys on a node's own type, and menu items are untyped child objects. Restoring edit to the fixture on top of this fix and re-running the gate gave exit 0, same 64 names — a dead spelling in a published fixture passes silently.

Filed as #5992, which also notes that this PR falsifies a measured parenthetical in the gate's header ("the fourth renders it as raw text"), left untouched here as out of fence.

Verification

Union re-run after the final commit, at 368f1c510:

checkresult
dropdown-menu-item-icon.test.tsx (new)7 passed
adjacent suites + catalog gallery (471 tests)3 files passed
check:icon-record-namesOK lucide icon names: 64 authored/declared names ... are live
check:control-bytes✅ check-control-bytes: OK (scanned 4958 tracked text file(s))
check:changeset-presence✅ 2 source file(s) ... declares 1 changeset(s)
check:doc-types✅ Every documented component type is registered.
check:doc-snippetsEvery covered documentation snippet compiles against the built types.
@object-ui/componentstype-checkexit 0
eslint (both changed sources)0 errors

check:doc-snippets first refused as a blind instrument (unbuilt sibling packages); the full workspace was built (43/43 tasks) and it was re-run rather than declared narrowed.

Reverse-verification. With the fix committed, both Icon JSX sites were reverted to the pre-fix <span> form under a trap ... EXIT INT TERM restore. Mutation confirmed on disk against the exact text targeted (injected pre-fix span present ×2, post-fix JSX absent ×0) — not by an editor exit code. Predicted direction red, observed 5 failed | 2 passed; the two survivors are the no-icon case and the retired-spelling-renders-nothing half, neither of which depends on the fix in the failing direction. Restore leg verified in both directions with a clean git status.

The suite asserts both directions — the glyph appears and the bare name does not — because an svg-only assertion passes against the broken renderer too.

Scope

Fence respected. context-menu.tsx and menubar.tsx were checked and read icon nowhere at all, so they carry no narrower version of this defect. Nothing under examples/schema-catalog/src/schemas/plugin-dashboard/ was touched, and no catalog-wide gate was added.

Generated by Claude Code


Generated by Claude Code

os-litantand others added 2 commits August 24, 2026 11:03
…ring the name as text
Both arms of `renderMenuItems` rendered an item's authored `icon` string into a
text node, so the catalog fixture named `with-icons.json` drew the words
"edit"/"copy"/"trash" beside its labels. Route both arms through `resolveIcon`
— the lucide RECORD surface `ui:button` and `action:*` already use — so an
unknown or retired spelling renders nothing rather than a word or a wrong glyph.
The fixture's `edit` is a retired spelling absent from lucide's runtime `icons`
record; replaced with `square-pen`, the live key it resolves to by identity.
Part of #5930
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
`react-hooks/static-components` does not fire inside `renderMenuItems` — it is
a plain helper, not a component — so both directives reported as unused. The
sibling `action:menu` sites keep theirs because there the resolution happens
inside a component body.
Part of #5930
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3232.6 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-DGJTXO3S.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)504.91KB114.42KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)165.30KB45.79KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.00KB61.86KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.10KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

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

Projects

None yet

2 participants

@yinlianghui@os-litant