Skip to content

Object Settings: an icon picker instead of a bare Lucide name input #12531

Closed

Description

@objectstack-fleet

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

  1. 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.
  2. The same picker component for the app's navigation item icon in AppNavInspector.tsx when that inspector exposes an icon field.
  3. The picker lives in packages/components if 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

  1. Tests: typing build lists building; picking it writes the name to the field; an unknown name shows the warning.
  2. Screenshots of the picker open and of the warning, light and dark.
  3. pnpm --filter @object-ui/app-shell test and, if the component moves there, pnpm --filter @object-ui/components test green.

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.

settings icon text input
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

  1. added
    enhancementNew feature or request
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Oct 15, 2026
  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (priority:p2; a Studio test-pass card under objectui#12446)
    Session: session_01JZnppgHRp4sREcouhkQYDB
    Account: sunhaolin
    Branch: claude/issue-12531-icon-picker
    Worktree: objectui-issue-12531
    Domain: domain:ui
    Seat: domain:ui#1
    File surface:

    • packages/app-shell/src/views/metadata-admin/inspectors/ObjectDefaultInspector.tsx: the Icon field (:94 on main 92318202a1).
    • 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.
    • The tests beside them; one .changeset/12531-*.md.

    The card asks for "a new IconPicker component", but main already has one: IconPickerWidget, which ActionDefaultInspector.tsx:755 and StudioDesignSurface.tsx:2484 mount. 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/objectui names no path-derived mandate)
    Clause-②: no
    Responsibility: n/a — not a defect card
    Thread-read: none
    Serial constraints cleared: read at objectui main 92318202a1.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T18:49Z

    • Merged: PR feat(app-shell): the object and app-nav icon fields pick from the icon catalog, and the picker warns on an unknown name (objectui#12531) #12558 landed through the merge queue as main 93ce81ac03 at 2026-10-15T18:45Z. It has one parent, 5f8ecf6781 (objectui#12548's landing), and is an ancestor of origin/main.
    • Verified by content: all 10 files the PR changes are blob-identical on 93ce81ac03 to the accepted head 62e216dfb5.
    • Mis-close scan: the PR body's only closing keyword is Fixes #12531. The only issue closed with the merge is this card, as completed.
    • What now holds:
      • Studio's Object Settings › Basics › Icon and an app navigation entry's Icon are picked with the console's existing IconPickerWidget, not typed.
      • Every mount of that picker warns inline on a stored name that matches no icon.
      • The stored value is still a Lucide name string.
      • See ACCEPT 6199430883, with the dev's light and dark screenshots.
    • Folded elsewhere: the action editor's unnamed icon picker went to objectui#12510 (6199308787).
    • Serial note: seat 4's objectui#12519 merges main if it touches AppNavInspector.tsx.
    • The chain: claim 6198349306, report 6199225752, ACCEPT 6199430883.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions