Skip to content

feat: make ShellIcons safe to use next to a UI kit: suffixed components, Icon.* factory, @onclick guard - #1

Merged
Shewart merged 8 commits into
mainfrom
icons-collision-safety
Sep 27, 2026
Merged

Shewart merged 8 commits into
mainfrom
icons-collision-safety

Conversation

@Shewart

@Shewart Shewart commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Why

Integrating ShellIcons into an app that already uses a UI kit (ShellUI) broke down:

  • @using ShellIcons.Icons puts 1,555 short names into Razor's tag lookup, which collide with UI-kit components like Badge, Table and Router (RZ9985).
  • The obvious workarounds fail quietly. A namespace alias (@using Icon = ShellIcons.Icons → <Icon.Plus />) compiles but renders blank, and type aliases don't help either.
  • The only thing that worked was writing <ShellIcons.Icons.Plus /> in full at ~100 call sites.

This PR adds ways to use icons that can't collide, and documents which one to pick.

What changed

Two collision-safe APIs, both from @using ShellIcons

Form Example Use for
Suffixed components <ChevronRightIcon Size="16" /> Markup (the new default)
Icon.* factory @Icon.ChevronRight() Icons as values: RenderFragment parameters, lists built in C#
  • Suffixed components: every icon also gets a {Name}Icon component in the root ShellIcons namespace. The suffix keeps names clear of UI-kit components and tells readers it's an icon (lucide-react ships the same alias). Each one is an empty subclass of the existing component, so all parameters work and it adds almost nothing to the DLL.
    • One exception: Lucide's shell icon would be ShellIcon, which is already the dispatcher. It gets no suffixed form and stays reachable as Icon.Shell(). The generator reports this as info diagnostic SHELLICONS003.
  • Icon.* factory: one static method per icon, returning a RenderFragment. All methods share one helper instead of each inlining a lambda.
    • A call with no arguments returns a cached fragment, so @Icon.Plus() in a hot render path doesn't allocate.
    • Only arguments you actually set are passed as parameters.
    • The cache is per icon type, so it doesn't stop the trimmer from removing unused icons.
  • The flat ShellIcons.Icons components are unchanged apart from no longer being sealed.

@onclick on an icon now fails with a clear message

On a component, Razor passes <PlusIcon @onclick="Save" /> as a plain string attribute named @onclick, not as an event handler. That string gets copied onto the <svg>, and the browser rejects @onclick as an attribute name, which breaks rendering with a cryptic JS error. This affected every existing icon component.

IconCore now throws an InvalidOperationException explaining the two forms that work:

  • a wrapping <button @onclick="…"> (recommended, and what screen readers expect)
  • onclick="@(() => Save())" without the @

Size

ShellIcons.Blazor.dll (net9.0, Release)
Before this PR (factory with inline lambdas) 1.74 MB
After (shared factory helper + 1,554 suffixed components) 1.11 MB

Fixes

  • Removed the Microsoft.SourceLink.GitHub package from Directory.Build.props. It pulled in Microsoft.Build.Tasks.Git 8.0.0, which has a known vulnerability (NU1902). The .NET 8+ SDK includes Source Link, and the packed .nuspec still records the repository commit.
  • The README example passing ["@onclick"] in additionalAttributes was wrong; the key is "onclick". It's now covered by a test.
  • The .gitignore rule for .claude/ never matched, because gitignore doesn't allow a comment on the same line.

Docs

  • README: a four-way comparison of the APIs, guidance for component libraries (put icons inside buttons; use the factory for icon parameters), click handling, and the aliases that don't work.
  • Docs site: the install and quick-start pages now teach @using ShellIcons + <ChevronRightIcon />. The /icons browser copies the suffixed name (@Icon.Shell() for shell).
  • CHANGELOG updated under Unreleased.

Tests

  • New .razor test page that imports a fake UI kit with a clashing Badge, plus Blazor's Routing namespace, and uses <BadgeIcon />, <RouterIcon /> and @Icon.Table() together. If a suffixed name ever collides, the test project stops compiling.
  • New tests for the suffixed components, the shell exception, factory caching, the onclick forms, and the @onclick error message. The test project now uses the Razor SDK so it can compile .razor files.
  • 64/64 pass (20 generator + 44 Blazor), zero build warnings. The docs site builds.

Compatibility

  • Nothing is removed. Existing <ChevronRight />, <ShellIcon Name="…" /> and Icon.* usage keeps working.
  • Behavior change: @onclick on an icon component now throws when it renders instead of breaking in the browser. That code never worked, so this only turns a cryptic error into a clear one.
  • Subclassing is now possible for ShellIcons.Icons.* types (no longer sealed).

@Shewart
Shewart merged commit 6e0ed61 into main Sep 27, 2026
1 check passed
@Shewart
Shewart deleted the icons-collision-safety branch October 1, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant