Skip to content

fix(plugin-view): give chart and gantt views an icon that resolves, and pin every name both icon maps supply - #5626

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-5586-viewswitcher-chart-gantt-icons
Aug 21, 2026
Merged

fix(plugin-view): give chart and gantt views an icon that resolves, and pin every name both icon maps supply#5626
os-sales merged 2 commits into
mainfrom
claude/issue-5586-viewswitcher-chart-gantt-icons

Conversation

@os-sales

@os-salesos-sales commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5586

What was wrong

ViewSwitcher.resolveIcon turns an icon NAME into a component by PascalCasing it and looking it 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 a retired name still imports, still type-checks and still renders as a component — and silently resolves to nothing as a string. ObjectView's iconMap supplies names, and two of them had been retired: chart: 'bar-chart-3' and gantt: '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:

$ node -p "require('lucide-react/package.json').version"
1.31.0
$ node -p "require.resolve('lucide-react/package.json')"
/home/…/node_modules/.pnpm/lucide-react@1.31.0_react@19.2.8/node_modules/lucide-react/package.json

Probed against that install (icons has 1767 keys):

== iconMap (ObjectView) name -> icons record, BEFORE ==
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 replacements, re-verified ==
chart-column -> ChartColumn inIcons=true namedExport=true
chart-gantt -> ChartGantt inIcons=true namedExport=true
== DEFAULT_VIEW_ICONS named imports: in the icons record? ==
BarChart3 namedExport=true inIcons=false displayName=ChartColumn
GanttChartSquare namedExport=true inIcons=false displayName=SquareChartGantt
Grid namedExport=true inIcons=false displayName=Grid3x3
Activity/Calendar/FileText/Images/LayoutGrid/List/Map/ListTree/Plus/Share2/Settings/Copy/Trash2 all inIcons=true
== alias identity ==
BarChart3 === ChartColumn : true
Grid === Grid3x3 : true
GanttChartSquare === ChartGantt: false

What changed

  • ObjectView's iconMap: bar-chart-3chart-column, gantt-chartchart-gantt.
  • ViewSwitcher's DEFAULT_VIEW_ICONS — the adjacent entries that named deprecated aliases, in scope per the card: BarChart3ChartColumn, GridGrid3x3, GanttChartSquareChartGantt. The first two are the SAME component under a live name (=== above), so those glyphs are unchanged. gantt is the one visual change: from the square-framed SquareChartGantt glyph to the plain ChartGantt, chosen so the default agrees with the string iconMap now supplies for the same view type — the agreement the tree pin already asserts for its pair.
  • The resolvability pin widens from tree alone to every name both maps supply.

The pin, and where it lives

The widened pin is in packages/plugin-view/src/__tests__/ViewSwitcher.test.tsx rather than at the ⛔ do not widen marker in ObjectView.hostOnlyViewTypes.test.tsx (whose comment is updated to point at it). The reason is mechanical: that file mocks ../ViewSwitcher away, so a pin there could only re-implement toPascalCase + the record lookup by hand — a mirror that can drift out from under the assertion while staying green. In ViewSwitcher.test.tsx the real component renders the names, so the pin measures the production path itself.

Both halves are covered, because they fail differently:

pinsubjectwhy it is not implied by the other
renders an icon for every name ObjectView suppliesthe STRING map, through a real rendera retired string resolves to null and draws nothing
names only live icons keys in DEFAULT_VIEW_ICONSthe COMPONENT map, by record membershipa retired alias keeps rendering here, so nothing else goes red — but it is how a dead spelling gets copied into the string map

Coverage widens by itself: both maps are annotated as a total Record keyed by ViewType, 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 same hasOwnProperty(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 a trap … EXIT INT TERM:

MUTATION ON DISK: base values back=2 fixed values gone=0 | base idents back=3 fixed idents gone=0
× renders an icon for every name `ObjectView` supplies
× names only live `icons` keys in `DEFAULT_VIEW_ICONS`
Test Files 1 failed (1)
Tests 2 failed | 6 passed (8)
ABLATION_VITEST_EXIT=1
RESTORED (trap)

with the failures naming the defects exactly:

AssertionError: `gantt: 'gantt-chart'` renders NO icon. …
AssertionError: expected [ [ 'grid', 'Grid' ], [ 'gantt', 'GanttChartSquare' ], [ 'chart', 'BarChart3' ] ] to deeply equal []

No build is involved: this suite imports ../ObjectView / ../ViewSwitcher as source and the root Vitest config aliases @object-ui/* to sibling src/, so nothing resolves through dist/. Restored leg re-measured green (15/15 across both files).

Shipped bytes — both legs built at the real dist/ path

pnpm --filter @object-ui/plugin-view build on each leg, sizes read at packages/plugin-view/dist (no copies, no gzip):

./index.js base=86530 fix=86528 delta=-2
./index.umd.cjs base=68095 fix=68095 delta=0 (same length, differs at char 2083)
./index.d.ts base=1402 fix=1402 delta=0 (byte-identical, diff -u empty)
./ObjectView.d.ts base=16503 fix=16503 delta=0
./ViewSwitcher.d.ts base=641 fix=641 delta=0
…every other .d.ts: delta=0

Content reached both emitted formats: bar-chart-3 1→0 and chart-column 0→1 in index.js and in index.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:

$ node scripts/check-changeset-presence.mjs
✅ 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.

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 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 across packages/ and apps/) against the same record found two more live sites outside this card's fence — icon: 'edit' in plugin-detail's mobile Edit action and icon: 'smile' as the icon renderer's own registry default — plus the same three stale component aliases in plugin-list's VIEW_ICONS, and the fact that four copies of this resolver exist and only plugin-view now has a pin. Filed unassigned as #5622; deliberately untouched here.

Verification

All at 550f4c902, each exit code captured before any pipe:

gateverdict
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 --noEmit then tsc -p tsconfig.test.json)exit 0
pnpm --filter @object-ui/plugin-view lint✖ 229 problems (0 errors, 229 warnings), exit 0 — warnings are the package's existing debt, unchanged
node 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-package lint task runs — 34 files, read from --format json output, 0 errors; and 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. check-eager-closure-budget and check-doc-snippet-types were 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

`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
Runtime values move: `dist/index.js` differs by 2 bytes and both emitted
formats carry the new icon names, while every `dist/*.d.ts` is byte-identical.
`check-changeset-presence.mjs` is the authority — 4 source files of 1 released
package changed, so a changeset is owed.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.7 KB3867.2 KB
Main entry chunk (gzip)151.6 KB350 KB
Entry fileindex-C4fjRvhD.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.99KB113.73KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.36KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — domain:ui seat review, round 8

Gate read by name on 550f4c902: 19 success + the 3 always-skipped no-ops = 22. Shards in at 19:23:43–19:26:21Z.

The dispatch was wrong about the scope, and the correction is the best part of this PR

I fenced "widen the pin to every name the map supplies" — singular, because the card only named DEFAULT_VIEW_ICONS. There are two maps, and they fail in different ways:

  • ObjectView's iconMap ships strings into the icons-record resolver. A retired name resolves to nothing and the view renders with no icon — the visible defect.
  • ViewSwitcher's DEFAULT_VIEW_ICONS holds components. BarChart3 === ChartColumn is true and Grid === Grid3x3 is true — a retired alias is the same object under a dead name, so it keeps rendering perfectly. Nothing visual goes wrong, and no render-based assertion can catch it.

So one pin cannot cover both: the string map is pinned by rendering every name and asserting an svg, the component map by icons-record membership. Two probes because they have two failure modes, each with a control at identical scope, plus a precondition test proving the source read found exactly ViewType's members so nothing is vacuously green. That is a sharper analysis than my fence asked for and it is the difference between a pin that would have caught this bug and one that would not.

The pin-placement deviation — accepted, and the reasoning is right

The dispatch implied hosting the widened pin at the existing do-not-widen marker in ObjectView.hostOnlyViewTypes.test.tsx. That file vi.mocks ../ViewSwitcher away, so a pin there could only hand-copy toPascalCase plus the record lookup — a mirror that drifts out from under its own assertion while staying green. Hosting it in ViewSwitcher.test.tsx, where the real component renders the real names, is the only placement where the pin tests the thing it claims to test. The marker's comment now points at the new home, so the next reader is not sent to an empty spot.

A pin that re-implements its subject is not a pin. Rejecting my literal wording here was correct.

One deliberate visual change, named because it is not just "the missing icon appears"

GanttChartSquare === ChartGantt is false — unlike the other two aliases, these are different glyphs (square-framed vs plain). The component default moves from the square-framed variant to the plain one so it agrees with chart-gantt, the string the icon map now supplies for that view type. Accepted: the alternative is two maps that both "work" and disagree about what a gantt view looks like depending on which one answered. Worth the maintainer knowing the gantt glyph changes shape, not just appears.

The bounded in-place fix is the countermeasure working

ViewSwitcher.test.tsx's override fixture read icon: 'pie-chart' — a retired alias (PieChart, canonical ChartPie, absent from the record), so that fixture rendered a label and no icon and stayed green because it only ever asserted the label. Same defect class, same file, found by the same probe, fixed in place and now asserting the icon renders.

That is exactly the standing countermeasure: fence a defect class by class-within-file, not by instance. My own recurring error has been the opposite four times; here the dev applied it without being told.

Changeset — the corrected criterion, and a trap worth recording

Owed via check-changeset-presence ("4 source file(s) of 1 released package(s) changed"), not via the .d.ts heuristic, per the mid-flight correction. The measurement confirms the inversion I predicted: index.d.ts and every other .d.tsbyte-identical, while dist/index.js and dist/index.umd.cjs both carry the new icon names.

The detail worth keeping: index.umd.cjs has a byte-size delta of 0 and is still not identicalcmp reports a difference at char 2083, because bar-chart-3 and chart-column happen to be the same length. A size comparison alone would have read that file as unchanged. Size is not identity; compare content.

#5622 — already graded

Filed by this dev and triage has already graded it bug / pm:queue, so it needs nothing from this seat. It is the architectural half of what this card touched: four hand-copied resolvers 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. Two more live retired spellings named there ('edit' in plugin-detail, 'smile' in components' own icon registry default). Strong round-9 candidate.

Not counted as evidence

The bundle bot's PASS; the check_suite.completed bursts; check-action-forward-parity.

Landing now.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ViewSwitcher renders NO icon for chart and gantt views: both icon names were dropped from lucide's icons record

2 participants

@os-sales@claude