feat(icons): add brand icon picker from dashboard-icons

Add a second tab in the node Icon picker to choose from the ~2250 brand
icons hosted by homarr-labs/dashboard-icons (Plex, Sonarr, Home
Assistant, etc.) served via jsDelivr CDN. The Generic tab keeps the
existing lucide picker unchanged.

Storage uses a 'brand:<slug>' prefix on custom_icon, so existing nodes
referencing lucide keys keep working with zero migration. A new
resolveCustomIcon helper returns a discriminated union (lucide | brand)
and a NodeIcon component centralizes rendering for both kinds.

Includes a manifest fetch script (scripts/fetch-dashboard-icons.mjs)
and a checked-in dashboardIcons.json snapshot.
This commit is contained in:
Pouzor
2026-05-11 19:18:01 +02:00
parent 3a9b3b2650
commit 3b0dbd7a8b
10 changed files with 292 additions and 7 deletions
+36 -2
View File
@@ -179,11 +179,45 @@ export const NODE_TYPE_DEFAULT_ICONS: Record<NodeType, LucideIcon> = {
text: Type,
}
/** Resolve the display icon for a node — custom_icon takes priority over type default. */
/** Resolve the display icon for a node — custom_icon takes priority over type default.
* Legacy: returns a LucideIcon component. Brand icons must use `resolveCustomIcon`. */
export function resolveNodeIcon(
typeIcon: LucideIcon,
customIconKey?: string,
): LucideIcon {
if (customIconKey && ICON_MAP[customIconKey]) return ICON_MAP[customIconKey]
if (customIconKey && !customIconKey.startsWith('brand:') && ICON_MAP[customIconKey]) {
return ICON_MAP[customIconKey]
}
return typeIcon
}
export const BRAND_ICON_PREFIX = 'brand:'
export function isBrandIconKey(key: string | undefined | null): boolean {
return !!key && key.startsWith(BRAND_ICON_PREFIX)
}
export function brandIconSlug(key: string): string {
return key.slice(BRAND_ICON_PREFIX.length)
}
export function brandIconUrl(slug: string): string {
return `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/${slug}.svg`
}
export type ResolvedIcon =
| { kind: 'lucide'; icon: LucideIcon }
| { kind: 'brand'; slug: string; url: string }
/** Resolve a node's icon to either a lucide component or a brand CDN URL.
* Used by renderers that support brand icons. Backwards-compatible with legacy
* string keys (no prefix → lucide lookup). Returns null when key unknown. */
export function resolveCustomIcon(customIconKey?: string): ResolvedIcon | null {
if (!customIconKey) return null
if (isBrandIconKey(customIconKey)) {
const slug = brandIconSlug(customIconKey)
return { kind: 'brand', slug, url: brandIconUrl(slug) }
}
const icon = ICON_MAP[customIconKey]
return icon ? { kind: 'lucide', icon } : null
}