Repository navigation
Object Settings: an icon picker instead of a bare Lucide name input #12531
Description
Source
Measured on objectui main d4bc81eab, 2026-10-15: Data pillar › Settings › Basics › Icon is a text input whose hint reads "Lucide icon name (e.g. building, users)" (designer.object.iconHint, metadata-admin/inspectors/ObjectDefaultInspector.tsx). The rail row and the item header's icon tile render whatever name is typed; a misspelt name renders nothing and nothing says so.
Target
- An icon picker: a popover with a search input and a grid of the Lucide icons the console ships, the current icon previewed inside the input, the name still typeable as the fallback; a name that matches no icon shows an inline warning and the tile falls back to the generic object glyph.
- The same picker component for the app's navigation item icon in
AppNavInspector.tsxwhen that inspector exposes an icon field. - The picker lives in
packages/componentsif a second host uses it, else beside the inspector.
File surface
packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.tsx, AppNavInspector.tsx, a new IconPicker component with its test, packages/app-shell/src/views/metadata-admin/i18n.ts.
Acceptance
- Tests: typing
buildlistsbuilding; picking it writes the name to the field; an unknown name shows the warning. - Screenshots of the picker open and of the warning, light and dark.
pnpm --filter @object-ui/app-shell testand, if the component moves there,pnpm --filter @object-ui/components testgreen.
Constraints
- The icon set is the one the console already bundles; no new icon package.
- The stored value stays a Lucide name string.
Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff from the 2026-10-15 Studio test pass: objectui main d4bc81eab, HotCRM 63db83e2, the vite console on :5180 against objectstack serve --dev on :4001, viewport 1482 × 1119, dark theme with a light-theme check.
Evidence
Screenshots from the test pass, kept on the temporary branch claude/studio-test-pass-evidence-20261015 of objectstack-ai/objectui (commit 72115588c, never merged: the maintainer ruled that screenshots do not enter main, and asked for a temporary branch referenced from the card). The branch's docs/design/studio-redesign/findings/2026-10-15/README.md maps every file to its card. Pane captures, 800 × 604 of a 1482 × 1119 viewport, dark unless named light.
![]()
docs/design/studio-redesign/findings/2026-10-15/12531-settings-icon-text-input.jpg — settings icon text input.
Parent: #12446
Body refreshed 2026-10-15T15:19Z by the epic PM session: an Evidence section with the test-pass screenshots added.
Activity
objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 1 (
priority:p2; a Studio test-pass card under objectui#12446)Session:
session_01JZnppgHRp4sREcouhkQYDBAccount:
sunhaolinBranch:
claude/issue-12531-icon-pickerWorktree:
objectui-issue-12531Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.tsx: the Icon field (:94onmain92318202a1).inspectors/AppNavInspector.tsx: the nav entry's icon field only (:448–:449).metadata-admin/widgets.tsx:IconPickerWidget(:2163), only if the card's unknown-name warning must be added there.metadata-admin/i18n.ts: new keys only..changeset/12531-*.md.The card asks for "a new
IconPickercomponent", butmainalready has one:IconPickerWidget, whichActionDefaultInspector.tsx:755andStudioDesignSurface.tsx:2484mount. This card reuses it. ⛔ No second picker.Outside the surface:
StudioDesignSurface.tsx,packages/components, the icon set the console bundles, and the stored value's shape (a Lucide name string) (stop on breach; explain in the report).Container & model:
S–M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuinames no path-derived mandate)Clause-②: no
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared: read at objectui
main92318202a1.AppNavInspector.tsx"only if the api name's move needs it". That is a different region from the icon field, so whichever lands second mergesmain.metadata-admin/i18n.ts, which is ordinary concurrency.widgets.tsxorObjectDefaultInspector.tsx(scanned in this act).Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 12531, "status": "done", "branch": "claude/issue-12531-icon-picker", "pr": "https://github.com/objectstack-ai/objectui/pull/12558", "session": "session_01JZnppgHRp4sREcouhkQYDB (subagent mode: the parent's id, as the claim and the container stamp it)", "premise_still_valid": true, "summary": "Both fields now mount the existing IconPickerWidget instead of a text input: ObjectDefaultInspector's Icon field (the object designer's Basics, mounted by Studio's Object Settings) and AppNavInspector's entry icon field (for entries where navEntryOffersField(entryType, 'icon') holds). No second picker was built. Each field is named by its Label (htmlFor + useId), passes its read-only posture through, and keeps its empty-value convention: the object field clears to icon: undefined, the nav entry to icon: ''. IconPickerWidget now shows an inline warning on a stored name that matches no icon: one new key, engine.form.unknownIcon, in en and zh. The trigger is aria-describedby the warning, and the warning replaces the field picker's '(not in object)' flag the trigger used to show. Every mount gets it, including the action inspector, the Studio nav inspector and SchemaForm's icon widget. designer.object.iconHint is retired from both locale blocks. Mechanism readings: A1 partly holds. The widget searches lucide-react/dynamic iconNames, the same set LazyIcon resolves, and previews the current icon. But it is a modal dialog, not a popover, and an unlisted name cannot be committed by typing: typing only filters, and a name is committed by picking. Neither was changed, per the widgets.tsx ruling. A2 holds, with the split above. A3: the hint is retired. The card's 'a misspelt name renders nothing' does not hold on main: the rail row and item header draw getIcon(name), which returns the Database glyph for an unknown name, the same glyph as for no name. That fallback is pinned, and the rail was not edited. Screenshots (not committed): /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-object-warning-light.png, /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-object-warning-dark.png, /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-nav-warning-light.png, /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-nav-warning-dark.png, /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-picker-open-light.png, /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-picker-open-dark.png, /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-object-picked-light.png, /tmp/claude-0/-home-user/c093033f-29b4-5e65-91e7-8316141c8317/scratchpad/shots-12531/12531-object-picked-dark.png.", "tests": "All runs at head 62e216dfb5 (main merged), from the repo root, through os-verify-lock. (1) The 28 suites `git grep -l -E 'IconPickerWidget|ObjectDefaultInspector|AppNavInspector|ActionDefaultInspector' -- '*.test.ts*'` names, plus both IconPickerWidget files: 'Test Files 28 passed (28) / Tests 335 passed (335)', VERDICT command-exit 0. (2) 33 widget-registry and designer-i18n readers plus the census and ratchet suites on this subject (widget-dom-leak-sweep, product-neutral-renderer.ratchet, inspectorStrings.i18n-10696, unknownValueFlag.i18n-9652, getIcon.oneHelper-11679, ariaLabelLiteralCensus-12145, iconOnlyButtonCensus-12143, no-component-any-cast.ratchet, adr0054-ratchet, writeRefusalCensus-11583, ...): 'Test Files 33 passed (33) / Tests 686 passed (686)', exit 0. (3) After `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build`: `pnpm --filter @object-ui/app-shell type-check` (echoed as type-check: tsc --noEmit && tsc -p tsconfig.test.json) exits 0, and --listFilesOnly lists all five touched test files. New pins: IconPickerWidget.unknownName-12531, ObjectDefaultInspector.iconPicker-12531, AppNavInspector.iconPicker-12531. Updated: ObjectDefaultInspector.test (commit and read-only legs) and AppNavInspector.separatorFields-11847 (the CONTROL leg picks chart-bar). Ablation, after the implementation was committed: objectstack scripts/ablation-replace.mjs put InspectorTextField back in place of IconPickerWidget in ObjectDefaultInspector.tsx (anchor x1 to x0, blob 84b4270f6c10 to a99d7b0cc0da). Its pin went red as expected: 'Tests 5 failed (5)'. Restored to the HEAD blob 84b4270f6c10 with git diff HEAD empty. The full app-shell suite (card acceptance 3) is declared to CI's 16 test shards.", "gates": "objectstack dispatch-gates refuses a sister repo, so the list was derived by hand from objectui's package.json check:* scripts and ci.yml, lint.yml and changeset-guard.yml. Local runs at 62e216dfb5, each exit code captured before any pipe, all EXIT=0: check:i18n-keys, check:i18n-drift (designer table: 1 key added, 1 removed, 0 en values changed), check:i18n-designer-parity, check:i18n-dead-keys (report-only; neither touched key is listed), check:icon-record-names, check:designer-field-key-parity, check:metadata-write-doors, check:handler-key-reads, check:unreferenced-sources, check:control-bytes, check:new-line-citations (0 new citations), check:changeset-claims, check:pending-changeset-literals, check-changeset-presence, check-changeset-no-major, check-changeset-overwrite and check-changeset-fixed. ESLint (--no-inline-config) on the 9 changed TS/TSX files: 0 errors, 54 warnings, all on unchanged lines. check-governed-queue-guard --test: NOT GOVERNED. PR check-runs at report time (head 62e216dfb5): success for Build Docs, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check and label; skipped for Test (coverage), Test (coverage shard) and dependabot; in_progress for the other 28, including Lint, Type Check, Test (shard 1-16/16), Test (dist pins), Build & E2E, Bundle Analysis, Repo Gates, Line Citation Gate, Changeset Claim Re-read, Changeset Overwrite Report, Governed Surface Queue Guard, Spec Main Shape Gate (known red, advisory) and Live E2E (informational). Per the contract, convergence is the PM's.", "mcp_calls": "0. No MCP GitHub tool was called.", "api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs), executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectui/pulls (draft), then POST /repos/objectstack-ai/objectui/issues/12558/assignees [sunhaolin]. Read back: 9891 bytes sent, 9891 stored, identical; assignee listed. (2) This report: POST /repos/objectstack-ai/objectui/issues/12531/comments via scripts/pm/post-stamped.mjs. Not REST: 3 git pushes of the branch (the empty claim marker, the implementation 6f85235a2b, and the merge of main 62e216dfb5). REST reads only otherwise: the issue, its comments, the PR, and check-runs, through gh api.", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: public door = the action editor (ActionDefaultInspector, mounted by Studio's object actions and Setup's action designer). Its icon picker's combobox has no accessible name 'Icon', because the field's Label carries no htmlFor and the IconPickerWidget gets no id. Probe: screen.getByRole('combobox', { name: 'Icon' }) finds nothing there, and this PR's CONTROL test has to locate the trigger by aria-haspopup='dialog' · evidence: the Label beside the IconPickerWidget mount in ActionDefaultInspector.tsx (the 'engine.inspector.action.icon' label) has no htmlFor · dedupe words: action inspector icon label, IconPickerWidget accessible name, unnamed combobox action icon, htmlFor icon picker", "carrier: none (承接者:无) · noted in the PR's Acceptance notes, not filed: Setup's AppNavInspector clears an entry icon to '' (kept as found), while Studio's nav inspector removes the key. Both parse as NavigationItemSchema and both draw the target's fallback, so nothing differs that a user can see", "carrier: none (承接者:无) · noted, not filed: check:i18n-dead-keys reports engine.form.keep as confirmed-dead, pre-existing and untouched. IconPickerWidget's keep option titles itself with the stored value", "carrier: none (承接者:无) · noted, not filed: the picker's own strings follow the I18nProvider language (useMetadataLocale), not a host inspector's locale prop. Pre-existing on every mount, untouched" ], "files_changed": [ ".changeset/12531-object-nav-icon-picker.md", "packages/app-shell/src/views/metadata-admin/widgets.tsx", "packages/app-shell/src/views/metadata-admin/i18n.ts", "packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.tsx", "packages/app-shell/src/views/metadata-admin/inspectors/AppNavInspector.tsx", "packages/app-shell/src/views/metadata-admin/IconPickerWidget.unknownName-12531.test.tsx", "packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.iconPicker-12531.test.tsx", "packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.test.tsx", "packages/app-shell/src/views/metadata-admin/inspectors/AppNavInspector.iconPicker-12531.test.tsx", "packages/app-shell/src/views/metadata-admin/inspectors/AppNavInspector.separatorFields-11847.test.tsx" ], "line_budget": "n/a: the diff touches no line-budgeted surface (no skills/** and no governed ledger)", "deviations": [ "The report adds files_changed, line_budget and deviations to the os-dev template, because the brief says the PM reads them. The template's other fields are unchanged.", "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. AGENTS.md and the os-dev contract take precedence: the commits carry the model-free pair (Claude-Session plus Co-authored-by: Claude), and the PR body ends with the session-URL footer.", "The default message of the local, not-yet-pushed merge commit was amended to add that trailer pair. There was no force-push.", "Throwaway screenshot harness: apps/console/shots-12531.html, apps/console/src/shots-12531.tsx and a driver under node_modules were all untracked, never committed, and deleted after capture. The first stop of its vite server killed only the nohup wrapper PID, so the vite children (my own, port 5231) stayed alive and wrote a .vite cache into the worktree. git worktree remove (no --force) then left that directory behind. I killed those recorded child PIDs and deleted the leftover directory, which held only the vite cache. The worktree is gone and git worktree list no longer names it." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:uiseat 1 ·session_01JZnppgHRp4sREcouhkQYDB· 2026-10-15T18:49Zmain93ce81ac03at 2026-10-15T18:45Z. It has one parent,5f8ecf6781(objectui#12548's landing), and is an ancestor oforigin/main.93ce81ac03to the accepted head62e216dfb5.Fixes #12531. The only issue closed with the merge is this card, ascompleted.IconPickerWidget, not typed.6199430883, with the dev's light and dark screenshots.6199308787).mainif it touchesAppNavInspector.tsx.6198349306, report6199225752, ACCEPT6199430883.Generated by Claude Code