Skip to content

Add built-in Lucide icon library for node styling #1774

@kmcginnes

Description

@kmcginnes

Problem Statement

Users who want to assign meaningful icons to node types must currently upload SVG or raster files manually through a file picker. This is friction-heavy: users need to find, download, and prepare icon files externally before they can style their graph. There is no browsable icon library inside the application, despite the app already shipping Lucide icons for its own UI.

Solution

Add a searchable, browsable icon picker to the node styling dialog that exposes the full Lucide icon set. Users can search by name or keyword, click to select, and the icon is applied immediately. Custom file upload remains available for domain-specific icons not covered by Lucide.

Icons selected from the library are stored as lucide:<name> references and resolved at render time to SVG data URIs. This enables the icon picker to highlight the currently selected icon and allows config files to reference icons symbolically. The Lucide icon catalog is resolved using lucide-react/dynamicIconImports which provides per-icon lazy loading with Vite-compatible static analysis.

User Stories

  1. As a graph explorer user, I want to pick an icon from a built-in library when styling a node type, so that I don't need to find and upload SVG files externally.
  2. As a graph explorer user, I want to search the icon library by keyword (e.g., "plane", "building", "person"), so that I can quickly find a relevant icon.
  3. As a graph explorer user, I want to see a visual preview of each icon in the picker before selecting it, so that I can compare options.
  4. As a graph explorer user, I want the icon picker to load quickly without slowing down initial app startup, so that the app remains responsive.
  5. As a graph explorer user, I want to still upload a custom SVG or raster image if the library doesn't have what I need, so that I'm not limited to the built-in set.
  6. As a graph explorer user, I want the selected Lucide icon to render correctly both in the graph canvas (Cytoscape) and in sidebar/search result previews, so that styling is consistent everywhere.
  7. As a graph explorer user, I want icons to respect the node color I've configured, so that SVG icons use currentColor and match my color scheme.
  8. As a graph explorer user, I want the icon I selected to persist across sessions, so that my styling choices are preserved.
  9. As a graph explorer user, I want to change my icon selection at any time by reopening the styling dialog, so that I can iterate on my graph's appearance.
  10. As a graph explorer user, I want the icon picker to indicate which icon is currently selected, so that I know what's already applied.
  11. As a graph explorer user, I want to be able to reset the icon back to the default after selecting a Lucide icon or uploading a custom one.
  12. As a graph explorer user, I want nodes with Lucide icons to render their icons on page load without needing to open the styling dialog first.

Implementation Decisions

  • Storage format: The iconUrl field in VertexPreferencesStorageModel stores "lucide:<name>" for library icons. This preserves the symbolic reference, enabling the icon picker to highlight the current selection and enabling config files to reference icons by name. No schema change needed — the field is already string | undefined.
  • Icon image type for Lucide: When a Lucide icon is selected, iconImageType is set to "image/svg+xml" since all Lucide icons are SVGs.
  • Icon resolution at render time: A resolver function handles all iconUrl formats:
    • lucide:<name> — extracts the icon name, calls the per-icon lazy loader from lucide-react/dynamicIconImports, builds an SVG string, and returns a data URI. Results are cached so each icon is resolved at most once per session.
    • data:... — passthrough (existing uploads)
    • Any other string — passthrough (treated as a URL)
  • Why lucide-react/dynamicIconImports: This object maps every icon name to an explicit () => import(...) function. Because each import path is a static string literal inside the lucide-react library, Vite can statically analyze and produce a chunk per icon at build time. This avoids the need for a single large catalog chunk while still supporting fully dynamic icon name resolution at runtime.
  • Render-time integration (graph canvas): renderNode.tsx already fetches and processes SVG icons via React Query (iconQueryOptions). The resolver plugs into this existing pipeline — for lucide: references, the query function calls the resolver instead of fetching a URL. Caching (staleTime: Infinity) ensures each icon is resolved once.
  • Render-time integration (UI components): VertexIcon.tsx uses react-inlinesvg which accepts a URL or data URI as src. For lucide: references, the component resolves to a data URI before passing to the SVG renderer. This can use the same React Query cache or a local state pattern with the resolver.
  • Icon picker UI: A new IconPicker component rendered inside the existing NodeStyleDialog. It consists of a search input and a scrollable grid of icon previews. Search filters against Lucide icon names (and optionally tags/categories from Lucide metadata). Selecting an icon calls setVertexStyle({ iconUrl: "lucide:<name>", iconImageType: "image/svg+xml" }).
  • Current selection highlighting: The picker reads vertexStyle.iconUrl, checks if it starts with "lucide:", extracts the name, and highlights the matching grid item. This is only possible because the symbolic reference is preserved in storage.
  • Picker icon rendering: Each grid cell resolves its icon using the same dynamicIconImports loader. Icons load asynchronously with a placeholder shimmer while loading. HTTP/2 multiplexing handles the parallel micro-fetches adequately for the visible subset.
  • Picker performance: Show a limited set initially (e.g., 50 icons), expand via search. Consider virtualization if scroll performance is an issue with the full set.
  • Custom upload preserved: The existing file upload button remains in the dialog alongside the new picker, giving users an escape hatch for icons not in the Lucide set.
  • Backward compatibility: Existing stored iconUrl values (base64 data URIs from prior uploads) continue to work unchanged — the resolver treats any data: string as a passthrough.
  • Bundle impact on main chunk: Only the dynamicIconImports key map (~30KB) is imported statically. Actual icon SVG data is loaded on demand.

Testing Decisions

  • Tests should verify external behavior (what icons resolve to, what the user sees), not implementation details.
  • Icon resolver module: Unit tests covering all input formats (lucide:*, data URI, plain URL passthrough), error handling for invalid/unknown Lucide icon names, correct SVG construction and data URI encoding, and caching behavior (second call for same name returns cached result). This is pure logic with a simple interface — highly testable in isolation.
  • Icon picker component: Component tests verifying search/filter behavior, selection callback emits correct lucide:<name> format, current selection is highlighted when vertexStyle.iconUrl matches a Lucide icon, and graceful handling of the loading state for icon previews.
  • Cytoscape rendering: Not explicitly tested as part of this work — the existing renderNode tests and visual verification cover downstream rendering. The resolver is the testable boundary.
  • Prior art: follow patterns in existing test files under src/core/StateProvider/ and src/modules/ that use Vitest and the project's component rendering utilities.

Out of Scope

  • Server-hosted icon files or server config references to icons (separate work)
  • Edge icons (edges use line styles, not icons)
  • Icon animation or dynamic icon states
  • User-uploadable icon packs or icon set management
  • Lucide icon version pinning strategy or automatic update mechanism
  • Icon color customization independent of node color (icons always inherit node color via currentColor)
  • Default styling file system (defaultStyling.json mount, fetch, merge — separate issue)
  • Styling import/export UI (separate issue)

Further Notes

  • Lucide publishes ~1500 icons, all MIT licensed.
  • This feature creates the lucide:<name> storage convention that future work (unified server config, operator-authored default styling) can reference without additional client changes.
  • The dynamicIconImports approach from lucide-react is preferred over lucide-static because it leverages Vite's static analysis without requiring a single large catalog bundle. Each icon loads individually on demand (~1KB per icon chunk).

Metadata

Metadata

Assignees

Labels

customizationCustomization options for rendering graph data in non-default waysenhancementNew feature or requestready-for-agentfully specified, ready for an AFK agent
No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions