`ViewSwitcher.resolveIcon` looks an icon NAME up in lucide's runtime `icons`
record. lucide retires a spelling by dropping it from that record while keeping
it as a deprecated named export, so `ObjectView`'s `iconMap` entries
`chart: 'bar-chart-3'` and `gantt: 'gantt-chart'` resolved to nothing on
lucide-react 1.31.0 and both view types rendered with no icon at all, while
every sibling type had one. Nothing went red: no lucide symbol appears in that
map, so the compiler never looked.
- `iconMap`: `bar-chart-3` -> `chart-column`, `gantt-chart` -> `chart-gantt`.
- `DEFAULT_VIEW_ICONS`: the adjacent entries that named deprecated aliases move
to the spellings lucide's record carries — `BarChart3` -> `ChartColumn`,
`GanttChartSquare` -> `ChartGantt`, `Grid` -> `Grid3x3`. The first and third
are the same component under a live name; `gantt` picks up the plain gantt
glyph, which is what `iconMap` now supplies for that type.
- The pin widens from `tree` alone to EVERY name both maps supply, and is
hosted where the real component renders the names rather than where a
hand-copied PascalCase mirror could drift.
Fixes#5586
Fixes#5586
What was wrong
ViewSwitcher.resolveIconturns an icon NAME into a component by PascalCasing it and looking it up in lucide's runtimeiconsrecord. lucide retires a spelling by dropping it from that record while KEEPING it as a deprecated named export, so a retired name still imports, still type-checks and still renders as a component — and silently resolves to nothing as a string.ObjectView'siconMapsupplies names, and two of them had been retired:chart: 'bar-chart-3'andgantt: 'gantt-chart'. Both types drew a label with no icon while every sibling had one, and nothing went red because no lucide symbol appears in that map for the compiler to look at.Evidence — the record the code reads, not the module's export list
Installed version, read from the resolved package:
Probed against that install (
iconshas 1767 keys):What changed
ObjectView'siconMap:bar-chart-3→chart-column,gantt-chart→chart-gantt.ViewSwitcher'sDEFAULT_VIEW_ICONS— the adjacent entries that named deprecated aliases, in scope per the card:BarChart3→ChartColumn,Grid→Grid3x3,GanttChartSquare→ChartGantt. The first two are the SAME component under a live name (===above), so those glyphs are unchanged.ganttis the one visual change: from the square-framedSquareChartGanttglyph to the plainChartGantt, chosen so the default agrees with the stringiconMapnow supplies for the same view type — the agreement thetreepin already asserts for its pair.treealone to every name both maps supply.The pin, and where it lives
The widened pin is in
packages/plugin-view/src/__tests__/ViewSwitcher.test.tsxrather than at the⛔ do not widenmarker inObjectView.hostOnlyViewTypes.test.tsx(whose comment is updated to point at it). The reason is mechanical: that file mocks../ViewSwitcheraway, so a pin there could only re-implementtoPascalCase+ the record lookup by hand — a mirror that can drift out from under the assertion while staying green. InViewSwitcher.test.tsxthe real component renders the names, so the pin measures the production path itself.Both halves are covered, because they fail differently:
ObjectViewsuppliesnulland draws nothingiconskeys inDEFAULT_VIEW_ICONSCoverage widens by itself: both maps are annotated as a total
Recordkeyed byViewType, so their key set IS the union, and a precondition test asserts the source read found exactly that union's members (a silent parse miss would make the rest vacuously green).(Prose note: this paragraph and the out-of-scope one below were first written with angle-bracket generics and read back as though GitHub had stripped them. Measured afterwards against the REST payload: GitHub stored them intact — the elision was in the API client used to verify. The angle-bracket-free wording is kept because it reads the same; nothing about the code or the measurements changed.)
Each probe has a control at identical scope: the render probe's control renders an unresolvable name through the same component, same schema shape, same container-scoped
querySelector('svg'), and asserts NO icon; the record-membership probe's control applies the samehasOwnProperty(icons, …)predicate to a name the record does not carry.Ablation — the pin has teeth
Reverting ONLY the two source files to
origin/main(tests untouched), with the mutation confirmed on disk by anchored grep counts before the run, and restored by atrap … EXIT INT TERM:with the failures naming the defects exactly:
No build is involved: this suite imports
../ObjectView/../ViewSwitcheras source and the root Vitest config aliases@object-ui/*to siblingsrc/, so nothing resolves throughdist/. Restored leg re-measured green (15/15 across both files).Shipped bytes — both legs built at the real
dist/pathpnpm --filter @object-ui/plugin-view buildon each leg, sizes read atpackages/plugin-view/dist(no copies, no gzip):Content reached both emitted formats:
bar-chart-31→0 andchart-column0→1 inindex.jsand inindex.umd.cjs. So the type surface is unchanged and the runtime values shipped are not — which is the expected shape for a values-only fix, and a changeset is still owed. The authority agrees:One bounded in-place fix, declared
ViewSwitcher.test.tsx's "explicit label and icon override" fixture readicon: 'pie-chart'—PieChartis a retired alias (canonicalChartPie,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 readschart-pieand asserts the icon actually renders.Out of scope — filed, not fixed here
A repo-wide scan of every kebab-case icon-name string literal on an
icon:key (53 distinct names, 109 sites acrosspackages/andapps/) against the same record found two more live sites outside this card's fence —icon: 'edit'inplugin-detail's mobile Edit action andicon: 'smile'as theiconrenderer's own registry default — plus the same three stale component aliases inplugin-list'sVIEW_ICONS, and the fact that four copies of this resolver exist and onlyplugin-viewnow has a pin. Filed unassigned as #5622; deliberately untouched here.Verification
All at
550f4c902, each exit code captured before any pipe:pnpm exec vitest run …/ViewSwitcher.test.tsx …/ObjectView.hostOnlyViewTypes.test.tsxTest Files 2 passed (2)·Tests 15 passed (15)pnpm exec vitest run packages/plugin-view(whole package)Test Files 19 passed (19)·Tests 199 passed (199)pnpm --filter @object-ui/plugin-view type-check(tsc --noEmitthentsc -p tsconfig.test.json)pnpm --filter @object-ui/plugin-view lint✖ 229 problems (0 errors, 229 warnings), exit 0 — warnings are the package's existing debt, unchangednode scripts/check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed…pnpm changeset:check✅ All workspace packages are in the changeset fixed group.·✅ No changeset declares a major bump.node scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 4672 tracked text file(s); skipped 85 binary).Vitest was run from the repo root throughout (the package-cwd invocation is refused by the repo's own guard). The lint run is a declared narrowing of the repo-wide
pnpm lint: the universe is eslint's own resolution of.from the package directory — exactly what the per-packagelinttask runs — 34 files, read from--format jsonoutput, 0 errors; andeslint.config.jsdeclares no type-aware parser options (projectService/project:/parserOptions/tsconfigRootDirall absent,recommendedTypeCheckedcount 0), so this diff cannot move the verdict on any file it does not touch.check-eager-closure-budgetandcheck-doc-snippet-typeswere not run: both are the known-broken gauges that exit non-zero until a console build / workspace build writes their inputs.Generated by Claude Code
Generated by Claude Code