Repository navigation
finding(components): ui:menubar never reads an item's icon either — the third MenuItem-shaped container, and the one no census can see #6326
Description
Blocked-by: #5931
Found while repairing #6278 (ui:context-menu's unread icon). Filed unassigned, out of that card's fence — #6278 is scoped to context-menu and its 裁决 explicitly forbids generalising the fix across containers.
Status (2026-08-28): the blocker #5931 is now in the maintainer's decision inbox (
needs-user-decision, assignee cleared), with its four-axis block posted. Its fork — declare / retire / redesign theiconkey forbutton-group— is what decides this card too. ⛔ Still blocked, correctly; nothing to do here until that is ruled.
⚠️ One measurement from #5931's own run is worth carrying, because it kills the obvious answer for at least one container: a proposal to simply strip the key from a component structurally unable to carry an icon was refuted by rendering the fixture —icon-toolbar.jsonproduces 3 buttons with empty textContent and 0 svg, so stripping leaves it rendering nothing at all. Whatever is ruled, "just remove the key" is not automatically safe.
What was measured, at ad54f7587
packages/components/src/renderers/overlay/menubar.tsx contains 0 references to icon (case-insensitive 0 as well).
That is not a shrug about a key nobody declared. MenubarMenu.items is typed as MenuItem[] — the same interface dropdown-menu and context-menu items use, and it declares icon?: string:
// packages/types/src/overlay.ts
export interface MenubarMenu {
label: string;
items: MenuItem[]; // <- MenuItem.icon?: string
}content/docs/components/overlay/menubar.mdx documents it too:
interface MenubarItem {
label?: string;
value?: string;
icon?: string;
...
The renderer has three places an item is drawn, and none of them reads the key:
{item.label} // submenu trigger
{item.children.map((child, childIdx) => (
{child.label} // nested child
))}
{item.label} // leafSo ui:menubar is the third and last member of the MenuItem-shaped family, alongside the two already answered: dropdown-menu (repaired by #5930) and context-menu (repaired by #6278).
Why this one was invisible, and why that is the interesting part
#6278's table was built by enumerating authored icon strings in the schema catalog and grouping them by nearest typed ancestor. examples/schema-catalog/src/schemas/components-overlay-menubar/application-menubar.json authors zero icon keys, so menubar could never appear in that table — not because the renderer is fine, but because no fixture exercises the key.
The same blind spot applies downstream: scripts/check-lucide-icon-record-names.mjs has no 'menubar' census entry, and even if one were added, its min non-vacuity precondition would (correctly) fail, because the descent reaches nothing. A gate that judges authored names cannot see a documented key that nothing authors.
That makes this a declared-but-unread key of the #4632 / #4652 / #4721 class, not the "fixture draws the wrong thing" class — the failure is silent in both directions: an author following the published MenubarItem shape gets no glyph and no error, and no gate reports anything.
Not addressed here, and not addressed by
- finding(components):
ui:context-menunever reads an item's authoredicon— dropdown-menu's identical twin, left behind by #5930 #6278 —context-menuonly; its ruling forbids generalising across containers. - Catalog fixtures declare child
iconkeys that button-group, breadcrumb and command never read — two of them are named with-icons.json and render none #5931 — ownsbutton-group,breadcrumb,command; nowneeds-user-decisionon whether the key should exist at all for those. Menubar's question is the same one, which is why this card follows rather than pre-empts it. (⚠️ breadcrumbandcommandhave since been repaired — PR fix(components): resolve ui:breadcrumb and ui:command child item icons through the record resolver #6644 wired both throughresolveIcon, the record surface, resolved once per item above the page/link split.button-groupis the half still open.) - finding(examples): the live menubar demo teaches a
shortcutarray against astringslot, and its separator renders as a blank menu item #6249 — a different menubar defect (the demo'sshortcutarray against astringslot, and a blank separator item). Disjoint from the icon key. - Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935 — resolver consolidation, queued.
The open question, stated but not answered
Whichever way #5931 is decided for its containers probably decides this one too: either MenuItem.icon is honoured everywhere it is typed and documented (route through resolveIcon, the RECORD surface, as #5930 ruled and as #6644 has now done for two more containers), or the key is removed from the surfaces that do not read it. ⛔ This card should follow that answer, not pre-empt it.
Incidentally, the nested-child arm above also renders only one level deep and drops disabled, unlike the sibling renderers' recursion. Noted, not investigated. Per triage, that incidental rides the same fix if the answer is "honour".
Serial note for the eventual claim: #6327 (hold) and #5935 (queued) touch the same file — check the serial queue.
Activity
objectstack-fleet commentedon Sep 24, 2026
Claim: PM loop round 1 —
domain:uiexecution seat 2Session:
session_01LkCKMa5bvrw3L4ezcNXEXWBranch:
claude/issue-6326-menubar-item-iconWorktree:
objectui-issue-6326Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/components/src/renderers/overlay/menubar.tsx, its tests, the menubar catalog fixture/docs only if a pin needs an authoredicon, one.changeset/6326-…md(stop on breach; explain in the report)Container & model:
S (not mechanical — both arms and the one-level nested arm),mode:subagent,model: opus(default judgement tier;dispatch-gates.mjs --tierREFUSES for objectui — no path-derived mandate exists here)Clause-②: no
Thread-read: none
Serial constraints cleared:
objectui#6327(pm:on-hold, same directory — recursion consolidation; ⛔ not folded, this card repairs the menubar copy only) ·objectui#5935closed ·objectui#6249closed ·Blocked-by: objectui#5931closed (completed 2026-09-18T20:15Z) · no seat-1 claim inpackages/components/src/renderers/overlay/; zeroIntl./toLocalehits inmenubar.tsx(objectui#9909 census)Clause-②
no:MenuItem.icon?: stringis already declared inpackages/types/src/overlay.tsandMenubarMenu.itemsis typedMenuItem[]; making the renderer read a declared key widens no accepted set (declared-but-unread, the objectui#5930 / objectui#6278 family). Readings taken 2026-09-24T12:52Z.objectstack-fleet commentedon Sep 24, 2026
os-dev-report
{ "issue": 6326, "status": "done", "branch": "claude/issue-6326-menubar-item-icon", "pr": "https://github.com/objectstack-ai/objectui/pull/10246", "session": "session_01LkCKMa5bvrw3L4ezcNXEXW — seat domain:ui#2 (the PM claim 5814492095 names this session and branch; the claim was verified as the newest Claim: on the card before starting)", "premise_still_valid": true, "summary": "packages/components/src/renderers/overlay/menubar.tsx now resolves MenuItem.icon through resolveIcon (the lucide RECORD surface, as objectui#5930 and objectui#6278 ruled; LazyIcon not used) on every arm that draws an item: the top-level leaf, the submenu trigger and the submenu child. The item walk is now a recursion local to this file, so a submenu child that has its own children is drawn as a nested submenu at any depth. Before, it was drawn as a leaf and its children were dropped. The PR also adds an ## Icons section to content/docs/components/overlay/menubar.mdx and one patch changeset for @object-ui/components. dropdown-menu.tsx and context-menu.tsx were not touched (objectui#6278 no-generalise ruling; objectui#6327 on hold). No published type changed. Zone 2 results: (1) HELD. No removal ruling for menubar icon exists. #7077's ruling (comment 5534409692) covers button-group only, and it names menubar only as a sibling in the disabled/onClick convention. #5931's last comment routes only the button-group third to #7077. #6327 has 0 comments. Note that objectui/issues/7077 itself answers REST 404, but its comment thread is still readable. (2) HELD on the fresh base a78cd378c (origin/main had moved past a507334, and menubar.tsx was unchanged between the two). git grep -n -i icon on menubar.tsx gave exit 1 with 0 lines; the lit controls on the same pathspec gave label 10 and disabled 2. Items were drawn in 3 places. (3) SPLIT. The one-level-deep half was true and is fixed here: it is the same arm being edited and it stays within this file. The drops-disabled half is FALSE today, because the objectui#6346 rider already forwards disabled={child.disabled}. (4) DONE: a render test authors icons on each arm, red before the fix and green after. The first write-route push probe was green. The branch was merged with origin/main (7 upstream commits, none touching packages/components) before the PR opened. The worktree was cleaned: node_modules removed, then git worktree remove without --force exited 0.", "tests": "New suite packages/components/src/__tests__/menubar-item-icon.test.tsx, 10 rows, lucide not mocked. It asserts svg.lucide-NAME per authored name, uses the always-present chevron-right as the positive control on the submenu-trigger instrument, and uses the retired spelling edit as the no-glyph control. RED BEFORE the fix (test on the unfixed renderer, run from repo root): 'Tests 5 failed | 5 passed (10)'. The 5 failures are the top-level leaf, submenu-trigger, submenu-child, depth-2 trigger and grandchild rows, each failing on the glyph or chevron assertion ('expected null not to be null'); the grandchild row failed because Export was a leaf and so unreachable. GREEN AFTER: 'Tests 10 passed (10)'. ABLATION on the committed fix 3dd2a7b, each leg run under trap restore EXIT INT TERM: the mutation was checked on disk (marker grep -c 1, remaining icon renders grep -c 1); the restore used git -C ROOT checkout HEAD -- ABS_PATH; after restore the working blob 9fa4384 equalled the HEAD blob and git diff HEAD had 0 lines. Submenu-trigger render removed: 'Tests 2 failed | 8 passed' (the submenu-trigger row and the depth-2 trigger row). Leaf render removed: 'Tests 3 failed | 7 passed' (top-level leaf, submenu child, grandchild). No dist/ rebuild was needed: the suite imports the renderer by a relative path from source. FULL PACKAGE on 3dd2a7b: pnpm exec vitest run packages/components/ gave 'Test Files 287 passed (287) / Tests 2787 passed (2787)', VERDICT command-exit 0. The later merge touched 0 files under packages/components. RELATED on 3dd2a7b: 7 files, 451 passed (component-fixture-declared-keys, component-docs-disabled-inherited-7239, menu-item-union, check-handler-key-read-sites, check-doc-fence-languages, zod-mirror-parity, widget-dom-leak-sweep). FINAL HEAD 6d7e1e7, all exit codes captured to a file before any pipe: targeted tests (8 files: the new suite, the context-menu and dropdown-menu icon suites, the menu onclick and separator suites, component-fixture-declared-keys, menu-item-union, check-handler-key-read-sites) gave 'Test Files 8 passed (8) / Tests 144 passed (144)', exit 0. turbo run type-check --filter=@object-ui/components gave 9/9 successful, exit 0; tsc -p tsconfig.test.json --listFiles contains both the renderer and the new test (2 hits). eslint . in packages/components: exit 0, with 503 files judged in the --format json output, 0 errors, and both changed files present. The one warning in each changed file is no-explicit-any; the renderer's warning is pre-existing on base, and the test's ComponentType any mirrors the sibling suites. Lint is not type-aware (no projectService/project in eslint.config.js), so the diff cannot move the verdict for any untouched file. Gates, all exit 0: check-changeset-presence ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-claims ('No pending changeset names a file this change touches'), changeset-no-major, changeset-fixed, changeset-overwrite, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, test-path-roots, control-bytes, lucide-icon-record-names, doc-fence-languages, doc-links, doc-example-ids, doc-component-types, handler-key-read-sites (check:handler-key-reads), doc-example-types, doc-snippet-types ('672 of 672 block(s) judged, 0 failed'), new-cross-file-line-citations ('0 new citation(s)') and doc-example-shared-reader. Before the doc-example/doc-snippet gates could run, check:doc-examples first exited 2 with PRECONDITION NOT MET; this was cleared by building its prerequisite set (turbo build of the --build-filter, 35/35 tasks). The first attempt at that build got lock exit 99 (queue-timeout behind another seat's app-shell suite) and was retried under the same slot name. Not run locally: pnpm test (full farm) and root lint, both CI's.", "mcp_calls": "0 — no MCP GitHub tools used (reads via REST curl; writes via the fleet-write relay)", "api_writes": "3 — (1) git push of branch claude/issue-6326-menubar-item-icon (probe + 2 pushes; git, not REST); (2) POST /repos/objectstack-ai/objectui/pulls (draft, via scripts/pm/with-fleet.sh --via auto → fleet-write relay op pr_create, run 36005999473, landed as objectstack-fleet[bot], PR #10246; body read back byte-identical, 5961 bytes); (3) POST /repos/objectstack-ai/objectui/issues/6326/comments (this os-dev-report, via post-stamped). Label writes: 0 (objectui, none named by the dispatch).", "open_questions": [], "out_of_scope_findings": [ "carrier: objectui#6327 (three-copy menu recursion consolidation, pm:on-hold) · In all three menu renderers the submenu-trigger arm forwards no declared disabled: MenubarSubTrigger, ContextMenuSubTrigger and DropdownMenuSubTrigger are rendered with no disabled prop, while the leaf arm forwards item.disabled. This was read from source, not probed at runtime, so it is not a class a claim; it is noted in the PR's Acceptance notes and not filed · dedupe words: submenu trigger disabled, SubTrigger disabled, menu item disabled submenu", "carrier: none (承接者:无) · The menubar catalog fixture application-menubar.json authors zero icon keys, so check-lucide-icon-record-names has no menubar census entry, and adding one would trip its own min non-vacuity rule. The renderer is now pinned by the new suite. This is noted in Acceptance notes only; it is not a class a/b/c defect." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Review — ACCEPT · PR objectui#10246 (head
6d7e1e7)domain:uiseat #2,session_01LkCKMa5bvrw3L4ezcNXEXW. Checked against GitHub (PR metadata, file list, diff and check runs), not against the report's own account.main· body first lineFixes #6326· +273/−49packages/components/src/renderers/overlay/menubar.tsx, the newmenubar-item-icon.test.tsx,content/docs/components/overlay/menubar.mdx(an Icons section),.changeset/6326-menubar-item-icon.md(@object-ui/components: patch). ⛔dropdown-menu.tsx/context-menu.tsxuntouched, per objectui#6278's no-generalise rulingrenderMenubarItemsrecursion;resolveIcon(item.icon)is resolved once per item and read by BOTH the submenu-trigger arm and the leaf arm (objectui#5930's "both arms" ruling).disabled,onSelect → item.onClick,shortcutand the separator arm are carried over unchanged. No published type touched ⇒Clause-②: noholdsicon(objectui#7077 coversbutton-grouponly). Nested depth: the one-level walk was real and is fixed on the same arm; the "dropsdisabled" half was already false on main (objectui#6346 rider)Acceptance notes (not filed): the submenu-trigger arm forwards no
disabledin all three menu renderers. This was read from source only, not probed, and belongs with the consolidation question in objectui#6327 (pm:on-hold). The menubar catalog fixture still authors noicon, so the lucide census has no menubar entry; the new suite pins the renderer instead.readings taken 2026-09-24T13:32Z
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
✅ Landed — PR objectui#10246, squash
d7de534onmain, card cleaneddomain:uiseat #2,session_01LkCKMa5bvrw3L4ezcNXEXW. Verified by content onorigin/main, not by sha alone: inpackages/components/src/renderers/overlay/menubar.tsx,resolveIcon(item.icon)gives 1 hit andrenderMenubarItems(the recursive walk) 3; lit control in the same file:MenubarSubTrigger3. Closed byFixes; this stroke stripspm:dispatchedand the assignee, because auto-close removes neither.readings taken 2026-09-24T13:55Z
Generated by Claude Code
fix(components): ui:menubar draws an item's authored icon on every ar…