Skip to content

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

  1. Adopt lucide-react as the dashboard icon library (stroke-based, ~1500 icons, MIT, matches the existing inline-SVG aesthetic).
  2. Expose it to generated components via a second sealed global, Icon, at the same axis as React. frontend/src/widgets/CustomWidgetRenderer.tsx builds the factory as new Function("React", "Icon", body)(React, Icon). The Icon wrapper lives at frontend/src/components/icons.tsx and accepts a kebab-case name plus any standard SVG prop.
  3. Resolution order inside the wrapper: curated kebab map → dynamic kebab→PascalCase lookup against lucide-react's exports → fallback to <HelpCircle/> with a deduped console.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.
  4. No relaxation of _check_no_imports. Generated TSX still must contain zero import statements; Icon is 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.
  5. 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-react ships 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-react is 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 in backend/app/widgets/prompts/component_synthesizer.md. Both must agree — the prompt is what the LLM sees.

Cross-references