Repository navigation
ViewSwitcher renders NO icon for chart and gantt views: both icon names were dropped from lucide's icons record #5586
Description
Measured while implementing #5321 on origin/main @ f2158ec86. Filed unassigned, not claimed, and deliberately not fixed there — #5321 owns the missing tree KEY in the same map, and this is a different defect (a key that is present but whose VALUE no longer resolves), needing new icon names chosen rather than a key added.
What was measured
ViewSwitcher.resolveIcon (packages/plugin-view/src/ViewSwitcher.tsx) turns an icon NAME into a component by PascalCasing it and looking it up in lucide's icons record:
const iconName = toPascalCase(name);
const mapped = iconNameMap[iconName] || iconName;
return (icons as any)[mapped] || null;
Two of the names ObjectView's iconMap supplies do not exist in that record on the installed lucide-react@1.31.0. Probed directly against the installed package:
kanban kanban -> Kanban RESOLVES
calendar calendar -> Calendar RESOLVES
map map -> Map RESOLVES
gallery layout-grid -> LayoutGrid RESOLVES
timeline activity -> Activity RESOLVES
gantt gantt-chart -> GanttChart *** MISSING ***
grid table -> Table RESOLVES
list list -> List RESOLVES
detail file-text -> FileText RESOLVES
chart bar-chart-3 -> BarChart3 *** MISSING ***
tree list-tree -> ListTree RESOLVES
The cause is that lucide keeps the old spellings as deprecated NAMED EXPORTS while dropping them from the icons record that a by-name lookup reads. Measured on the same version:
| name | named export | in icons |
|---|---|---|
BarChart3 |
yes | no |
GanttChart |
yes | no |
GanttChartSquare |
yes | no |
ChartColumn |
yes | yes |
ChartGantt |
yes | yes |
So the compiler is no help here: ViewSwitcher.tsx imports BarChart3 and GanttChartSquare by name for its own DEFAULT_VIEW_ICONS and type-checks clean, while the by-name path beside it silently misses.
User-visible effect
getViewIcon does not fall back to DEFAULT_VIEW_ICONS once an explicit icon is set — it returns resolveIcon(view.icon), i.e. null:
function getViewIcon(view: ViewSwitcherItem): LucideIcon | null {
if (view.icon) {
return resolveIcon(view.icon);
}
return DEFAULT_VIEW_ICONS[view.type] || null;
}
and the render site is {Icon ? ( Icon ) : null}. ObjectView always supplies an icon string (icon: iconMap[v.type] || 'table'), so a host-composed chart or gantt view is drawn as a label with no icon at all, while every other type in the same switcher has one. No crash. The console reaches this: CreateViewDialog offers both chart and gantt among the view types a user can create, and those records arrive as ObjectView's views prop.
Why it is worth a card rather than a drive-by
Picking the replacements is a judgement call, not a mechanical rename — lucide offers several candidates per glyph (ChartColumn, ChartColumnBig, ChartGantt, ChartNoAxesGantt), and the choice should probably be made together with the DEFAULT_VIEW_ICONS entries beside them, which still import the deprecated aliases. There is also a guard question worth settling in the same breath: nothing today fails when a name stops resolving, which is exactly why this survived a major lucide bump. A pin asserting that every name the map supplies resolves in icons would close the class; #5321 deliberately left its resolvability assertion scoped to tree alone and says so in a comment, so that the widening happens here with the fix rather than as a surprise red.
Not fixed in #5321
#5321's ruling was about which view types are authorable, and its one behaviour change was adding the missing tree KEY (tree: 'list-tree', verified resolving). Its scope fence is packages/plugin-view/src/** plus the record; changing two working-looking icon names on a judgement call is outside it.
Dedup
Searched open issues for the lucide-name shape, for the iconMap / view-switcher icon shape, and for lucide upgrade cards. Nothing open covers it. Related and closed: #2916 (added the chart key to this same map — the key it added is the one now failing to resolve), #1885 (tree view added). Related and open: #5321 (the missing tree key, and the host-only view-type record).
Activity
huangyiirene commentedon Aug 21, 2026
Triage (first-touch grading): promoted
finding→pm:queue,domain:ui+ bug, type Bug. Rationale: measured user-visible defect on a console-reachable path (CreateViewDialogofferschartandgantt; both render iconless while every sibling has an icon), with the root cause isolated to lucide dropping the old spellings from theiconsrecord while keeping deprecated named exports — which is also why the compiler stays green and why no gate caught the bump. The icon-name choice is dev judgment (pick from the card's candidate table together with the adjacentDEFAULT_VIEW_ICONSentries); the load-bearing half of the fix is the resolvability pin widened to every name the map supplies, which #5321 deliberately scoped totreealone so the widening could land here — that pin closes the class against the next lucide bump.Dispatch notes: same file surface as in-flight #5321 (
packages/plugin-view/src/**, sameiconMap) — hard serial after #5321 lands; fold-or-serial answer owed by the dispatching seat if #5321 is still open at claim time. Size/model suggestion: S–M.Generated by Claude Code
os-sales commentedon Aug 21, 2026
Claim —
domain:uiexecution seat (#5560), round 8.session_012u2pRjcqAYtoEjgr3wwhnKclaude/issue-5586-viewswitcher-chart-gantt-iconspm:queue→pm:dispatchedDispatch notes honoured from triage: the serialization concern against #5321 is cleared (that work has landed), and the load-bearing half of the fix is taken to be the resolvability pin widened to every name
DEFAULT_VIEW_ICONSsupplies — not justchart/gantt. Icon selection comes from the card's own verified-resolving candidates.Generated by Claude Code
fix(plugin-view): give `chart` and `gantt` views an icon that resolves
os-sales commentedon Aug 21, 2026
os-dev-report
{ "issue": 5586, "status": "done", "premise_still_valid": true, "branch": "claude/issue-5586-viewswitcher-chart-gantt-icons", "pr_url": "https://github.com/objectstack-ai/objectui/pull/5626", "head_sha": "550f4c902", "files_changed": [ "packages/plugin-view/src/ObjectView.tsx", "packages/plugin-view/src/ViewSwitcher.tsx", "packages/plugin-view/src/__tests__/ViewSwitcher.test.tsx", "packages/plugin-view/src/__tests__/ObjectView.hostOnlyViewTypes.test.tsx", ".changeset/view-switcher-chart-gantt-icons-5586.md" ], "what_changed": "ObjectView's iconMap: chart 'bar-chart-3' -> 'chart-column', gantt 'gantt-chart' -> 'chart-gantt'. ViewSwitcher's DEFAULT_VIEW_ICONS de-aliased in the same pass: BarChart3 -> ChartColumn, Grid -> Grid3x3 (both are the SAME component under a live name, so those glyphs are unchanged), GanttChartSquare -> ChartGantt (the one visual change: square-framed SquareChartGantt -> plain ChartGantt, so the default agrees with the string iconMap now supplies for that view type). The resolvability pin widens from `tree` alone to EVERY name both maps supply, hosted in ViewSwitcher.test.tsx (where the real component renders the names) rather than at the old do-not-widen marker in ObjectView.hostOnlyViewTypes.test.tsx, whose comment is updated to point at it - that file mocks ../ViewSwitcher away, so a pin there could only re-implement toPascalCase + the record lookup by hand, a mirror that can drift while staying green. Two probes because they fail differently: the STRING map is pinned by rendering every name and asserting an svg; the COMPONENT map is pinned by icons-record membership (a retired alias keeps rendering there, so nothing else would go red). Each has a control at identical scope, and a precondition test proves the source read found exactly ViewType's members so nothing is vacuously green.", "evidence": { "lucide_version": "lucide-react@1.31.0, read from the resolved package: node -p \"require('lucide-react/package.json').version\" -> 1.31.0 at node_modules/.pnpm/lucide-react@1.31.0_react@19.2.8/node_modules/lucide-react. The `icons` record has 1767 keys.", "icon_resolution_probe": "BEFORE (iconMap name -> icons record): gantt 'gantt-chart' -> GanttChart *** MISSING ***; chart 'bar-chart-3' -> BarChart3 *** MISSING ***; the other nine (kanban/calendar/map/gallery/timeline/grid/list/detail/tree) all RESOLVE. REPLACEMENTS re-verified on the installed version: chart-column -> ChartColumn inIcons=true; chart-gantt -> ChartGantt inIcons=true. DEFAULT_VIEW_ICONS named imports: BarChart3 inIcons=false (displayName ChartColumn), GanttChartSquare inIcons=false (displayName SquareChartGantt), Grid inIcons=false (displayName Grid3x3); the other 13 all inIcons=true. Alias identity: BarChart3 === ChartColumn true, Grid === Grid3x3 true, GanttChartSquare === ChartGantt false. ABLATION (revert only the two src files to origin/main, mutation confirmed on disk by anchored grep counts - 'base values back=2 fixed values gone=0 | base idents back=3 fixed idents gone=0' - restored by trap ... EXIT INT TERM): 2 failed | 6 passed, naming '`gantt: gantt-chart` renders NO icon' and [['grid','Grid'],['gantt','GanttChartSquare'],['chart','BarChart3']]. No build needed - the suite imports ../ObjectView and ../ViewSwitcher as source and the root Vitest config aliases @object-ui/* to sibling src/, so nothing resolves through dist/. Restore leg re-measured green 15/15.", "dist_deltas": "Both legs built with `pnpm --filter @object-ui/plugin-view build`, sizes read at the real packages/plugin-view/dist path (no copies, no gzip): index.js base=86530 fix=86528 delta=-2; index.umd.cjs base=68095 fix=68095 delta=0 but NOT identical (cmp: differs at char 2083); index.d.ts 1402/1402 byte-identical (diff -u empty); ObjectView.d.ts 16503, ViewSwitcher.d.ts 641, and every other .d.ts delta=0. Content check: bar-chart-3 1->0 and chart-column 0->1 in BOTH index.js and index.umd.cjs. So zero .d.ts movement with real movement in the emitted JS - the mirror image of the prose-card criterion, exactly as the PM's mid-flight correction predicted." }, "changeset": { "present": true, "file": ".changeset/view-switcher-chart-gantt-icons-5586.md", "bump": "patch", "why": "Owed by the authority named in the PM correction, not by the .d.ts heuristic. `node scripts/check-changeset-presence.mjs` verdict: '4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/view-switcher-chart-gantt-icons-5586.md.' (exit 0). Independently corroborated by the measurement: dist/index.js and dist/index.umd.cjs both carry the new icon names, so shipped runtime values move even though every .d.ts is byte-identical. patch, not minor/major - a bug fix, and the repo forbids major in the fixed group." }, "tests": { "command": "pnpm exec vitest run packages/plugin-view/src/__tests__/ViewSwitcher.test.tsx packages/plugin-view/src/__tests__/ObjectView.hostOnlyViewTypes.test.tsx AND pnpm exec vitest run packages/plugin-view (both from the REPO ROOT - package-cwd vitest is refused by the repo's own guard; both wrapped in the container's shared heavy-verify lock)", "result": "targeted: 'Test Files 2 passed (2)' / 'Tests 15 passed (15)'. whole package: 'Test Files 19 passed (19)' / 'Tests 199 passed (199)'. Targeted run re-measured at the final commit 550f4c902 after the changeset commit: same verdict." }, "gates": [ { "name": "vitest (2 targeted files, repo root)", "status": "pass - Test Files 2 passed (2), Tests 15 passed (15) @ 550f4c902" }, { "name": "vitest (whole plugin-view package, repo root)", "status": "pass - Test Files 19 passed (19), Tests 199 passed (199)" }, { "name": "pnpm --filter @object-ui/plugin-view type-check (tsc --noEmit; tsc -p tsconfig.test.json)", "status": "pass - exit 0, run after building the dependency closure with pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-view^...' build" }, { "name": "pnpm --filter @object-ui/plugin-view lint", "status": "pass - exit 0, verdict line 'x 229 problems (0 errors, 229 warnings)'; the warnings are the package's pre-existing debt and the count is unchanged" }, { "name": "node scripts/check-changeset-presence.mjs", "status": "pass - exit 0, '4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'" }, { "name": "pnpm changeset:check (check-changeset-fixed + check-changeset-no-major)", "status": "pass - exit 0, 'All workspace packages are in the changeset fixed group.' / 'No changeset declares a major bump.'" }, { "name": "node scripts/check-control-bytes.mjs", "status": "pass - exit 0, 'OK (scanned 4672 tracked text file(s); skipped 85 binary)'; plus a manual grep -naP scan of the four edited files for control bytes, no hits" }, { "name": "check-eager-closure-budget / check-doc-snippet-types", "status": "not run - the two known-broken gauges named in the dispatch (they exit non-zero until a console build / workspace build writes their inputs); not 'fixed', not claimed green" }, { "name": "CI (full farm)", "status": "in_progress - report delivered at draft-PR time per the standing contract; CI convergence is the reviewing seat's read" } ], "scope_fences_respected": true, "notes_for_review": [ "PIN PLACEMENT is the one deliberate deviation from the literal dispatch wording: the widened pin lives in ViewSwitcher.test.tsx, not at the do-not-widen marker in ObjectView.hostOnlyViewTypes.test.tsx. That file mocks ../ViewSwitcher, so a pin there would have to hand-copy toPascalCase + the icons lookup - a mirror that can drift out from under the assertion while staying green. The marker's comment is rewritten to point at the new home. Reject and I will move it.", "ONE BOUNDED IN-PLACE FIX, declared: ViewSwitcher.test.tsx's 'explicit label and icon override' fixture read icon: 'pie-chart'. PieChart is a retired alias (canonical ChartPie, inIcons=false), so that fixture rendered a label and NO icon and stayed green because it only ever asserted the label. Same defect class, same file, mechanically determined by the same probe: it now reads 'chart-pie' AND asserts the icon renders.", "LINT NARROWING, declared: the repo-wide pnpm lint was narrowed to the affected package. Universe = eslint's own resolution of '.' from the package directory, which is exactly what the per-package lint task runs; 34 files, count read from --format json output; 0 errors. Invariance: eslint.config.js declares no type-aware parser options (projectService / project: / parserOptions / tsconfigRootDir all absent, recommendedTypeChecked count 0), so this diff cannot move the verdict on any file it does not touch. CI runs the full farm regardless.", "The PR body's attribution footer uses the session-URL form (_Generated by [Claude Code](https://claude.ai/code/session_...)_) rather than the bare form: the bare form is stripped from a PR body on every subsequent edit, and this body was edited once already (see next note). Same footer text, link carries the session.", "CORRECTED MEASUREMENT - NOT a GitHub sanitizer event. The first version of the PR body read back as though two angle-bracket fragments had been eaten (Record<ViewType, ...> and icon: '<kebab>'), so they were rewritten without angle brackets. Reading both bodies back through the REST API afterwards shows GitHub stored every fragment INTACT: the elision happens in the MCP issue/PR read path used to verify, which strips angle-bracket fragments from the body it returns. Same path hides the HTML-comment report marker, which is why this comment now also leads with the literal text os-dev-report. The PR-body rewrite is left in place (equally accurate prose) and its own note corrected." ], "out_of_scope_findings": [ "filed as #5622: two more retired lucide spellings reach the icons-record resolver on live paths outside plugin-view - icon: 'edit' in plugin-detail/src/DetailView.tsx:784 (the mobile Edit action, rendered through an action:bar and components' resolve-icon.ts; Edit is absent from the record, canonical square-pen) and icon: 'smile' in components/src/renderers/basic/icon.tsx:83 (the icon renderer's OWN registry default and its name input's defaultValue; Smile is absent, canonical face-slightly-smiling). Also recorded there: plugin-list/src/ViewSwitcher.tsx names the same three stale component aliases (Grid, GanttChartSquare, BarChart3); four hand-copied resolver implementations read the icons record while app-shell's getIcon and layout's resolveIcon read lucide/dynamic's iconNames (2025 entries, which DOES carry the retired aliases), so the same name renders in the sidebar and renders nothing in an action bar; and only plugin-view now has a resolvability pin. Filed unassigned, no labels, for triage." ], "open_questions": [] }Generated by Claude Code
fix(plugin-view): give `chart` and `gantt` views an icon that resolve…