Catalog & source generator
The typed icon components you use (<div data-shelldocs-slot="component" data-shelldocs-id="s86632b1651f8"></div> and friends) aren't hand-written — they're emitted by a Roslyn source generator at every build. This page explains how it works and what to touch if you're contributing to the repo.
Two source packs
catalog/
├── lucide/ <- vendored, refreshed by scripts/sync-lucide.ps1
│ └── icons/
│ ├── chevron-right.svg
│ ├── chevron-right.json (Lucide metadata)
│ └── ...
└── custom/ <- repo-owned, hand-authored
└── icons/
├── house-filled.svg (optional — none by default)
└── ...
Both feed the same generator. Both produce components in the same ShellIcons.Icons namespace. Consumers can't tell an icon from Lucide apart from an icon from custom.
The generator pipeline
ShellIcons.Blazor.csprojmarkscatalog/lucide/icons/*.svgandcatalog/custom/icons/*.svgas<AdditionalFiles>.ShellIcons.Generatorreads them viacontext.AdditionalTextsProvider.- For each SVG:
SvgParser.ExtractInnerstrips the outer<svg>wrapper, preserving the inner<path>/<circle>/etc. verbatim.Naming.KebabToPascalturnschevron-rightintoChevronRight.ResolvePacktags the icon"lucide"or"custom"based on file path.
- Collision resolution — custom-pack icons win over same-name Lucide icons. A
SHELLICONS001info diagnostic is logged. - Emit, per icon: a component in
ShellIcons.Icons, its{Name}Iconalias inShellIcons, and anIcon.{Name}()factory method. Plus oneShellIcondispatcher for the whole catalog:
// Icons/ChevronRight.g.cs (auto-generated)
public class ChevronRight : global::ShellIcons.IconCore
{
protected override string IconName => "chevron-right";
protected override void EmitChildren(RenderTreeBuilder builder, int seq) =>
builder.AddMarkupContent(seq, @"<path d=""m9 18 6-6-6-6"" />");
}
Refreshing the Lucide catalog
LUCIDE_VERSION.txt at the repo root pins the upstream tag. To bump:
./scripts/sync-lucide.ps1 # sync at pinned version
./scripts/sync-lucide.ps1 -Version 0.480.0 # bump + repin
The script downloads the Lucide GitHub archive at that tag, extracts icons/, replaces catalog/lucide/icons/ in one atomic step, and updates LUCIDE_VERSION.txt.
Rebuild after. The generator picks up the diff automatically — new icons emit, changed icons emit updated shape data, dropped icons vanish.
Diagnostics
| ID | Severity | When |
|---|---|---|
SHELLICONS001 |
Info | A catalog/custom/ icon has the same name as a catalog/lucide/ icon. Custom wins; the message logs the override. |
SHELLICONS002 |
Warning | MAUI only: an icon uses SVG the XAML targets can't draw (<g>, transform, …). That part is skipped. |
SHELLICONS003 |
Info | An icon has no {Name}Icon alias because the name is taken (Lucide's shell → ShellIcon, the dispatcher). Use Icon.Shell(). |
SHELLICONS004 |
Warning | MAUI only: an icon name can't become an enum member (e.g. none, or starts with a digit). It's skipped. |
SHELLICONS005 |
Info | MAUI only: a shape lies entirely outside the 24×24 viewBox and is dropped. |
Debugging the generator
Set <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> in ShellIcons.Blazor.csproj and rebuild. Generated files land at obj/Debug/net9.0/generated/ShellIcons.Generator/…/Icons/*.g.cs. Compare the emitted code against your expectations if things don't render.
Naming edge cases
- Lucide 0.475.0 ships no numeric-prefix icons (e.g.
1st-place-medal), so we don't ship a NumberPrefix map yet. If a future Lucide bump introduces one, we'll port Lucide's own map —"1st" → "firstPlace". - Aliases (e.g.
home→house) aren't emitted as separate components. On MAUI,IconCatalog.TryParse("home", …)resolves them; the Blazor package doesn't use them yet.
Trimming
- Typed icons,
{Name}Iconaliases andIcon.*factory methods — only the icons you reference survive ShellIcondispatcher — referencing it (orShellIcon.Names) roots the entire dictionary
MAUI
ShellIcons.Maui has its own generator, ShellIcons.Generator.Xaml, which reads the same SVGs plus the JSON sidecars. It converts each icon to a single path (see How rendering works) and emits the IconName enum, IconCatalog metadata and one typed control per icon.