ADR-010: Icon library injection (lucide-react via a sealed Icon global)¶
Status: Accepted Source: prd.md §19
Context¶
ADR-006 sealed the custom-widget renderer scope to a single injected global, React. That kept the in-browser eval surface tiny but starved both the dashboard chrome and the LLM-generated components of icons — every glyph was an inline <div><svg> path, a Unicode character (▲ ▼ + − ⟲ × !), or a single-letter chip. The chrome looked unfinished against the design reference, and the synthesizer prompt could not steer the LLM toward semantic icons because there was nothing to steer it toward. Importing lucide-react directly inside generated TSX is forbidden by _check_no_imports in backend/app/widgets/validators.py.
Decision¶
- Adopt
lucide-reactas the dashboard icon library (stroke-based, ~1500 icons, MIT, matches the existing inline-SVG aesthetic). - Expose it to generated components via a second sealed global,
Icon, at the same axis asReact.frontend/src/widgets/CustomWidgetRenderer.tsxbuilds the factory asnew Function("React", "Icon", body)(React, Icon). TheIconwrapper lives atfrontend/src/components/icons.tsxand accepts a kebab-casenameplus any standard SVG prop. - Resolution order inside the wrapper: curated kebab map → dynamic kebab→PascalCase lookup against
lucide-react's exports → fallback to<HelpCircle/>with a dedupedconsole.warn. Any of the ~1500 Lucide icons resolves at runtime; the curated ~80 names are simply faster and act as the LLM's strong default palette. - No relaxation of
_check_no_imports. Generated TSX still must contain zeroimportstatements;Iconis a global, not an import. The synthesizer prompt (backend/app/widgets/prompts/component_synthesizer.md, v1.3.0) lists the curated names plus the usage signature. - Replace every visible inline-SVG / Unicode glyph across the dashboard chrome — Sidebar, Header, KpiTile, AlertsFeed, RecommendationsPanel, CustomerDevicePanel, ConnectedSystems, UsHeatmap, MyWidgetsRail, WidgetBuilderModal — with
<Icon name="..."/>. Lucide ships no Slack / Confluence brand glyphs; use semantic stand-ins (message-square,book-text) paired with brand color rather than pulling in a second icon package.
Consequences¶
- Generated custom widgets can render real severity icons, KPI deltas, and category glyphs without breaking the no-imports contract. The synthesizer prompt is now icon-aware (v1.3.0).
lucide-reactships at ~150 KB gzipped (full library, no tree-shaking because we use dynamic name lookup). Acceptable for the hackathon density target; tree-shake via codegen is a future optimisation if bundle size matters.- Brand-mark fidelity for Slack / Confluence is intentionally not perfect (no Lucide brand pack). Adding
simple-icons-reactis straightforward when needed. - Adding a new icon name to the curated list requires editing both
frontend/src/components/icons.tsx(CURATED_ICON_NAMES) and the catalog block inbackend/app/widgets/prompts/component_synthesizer.md. Both must agree — the prompt is what the LLM sees.
Cross-references¶
- ADR-006 — established the sealed-global pattern this ADR extends.
- Implementation: frontend/src/components/icons.tsx, frontend/src/widgets/CustomWidgetRenderer.tsx.