import { loadIcon } from '@iconify/core/lib/api/icons' import { setAPIModule } from '@iconify/core/lib/api/modules' import { fetchAPIModule } from '@iconify/core/lib/api/modules/fetch' import { iconToSVG } from '@iconify/utils' import type { IconifyIcon } from '@iconify/types' import type { ReactElement, ReactNode } from 'react' // Initialize API module setAPIModule('', fetchAPIModule) export interface IconData { svg: string width: number height: number body: string viewBox: { left: number; top: number; width: number; height: number } } const iconCache = new Map() // Raw icon data cache (before size transformation) const rawIconCache = new Map() /** * Load raw icon data (without size transformation) */ async function loadRawIcon(name: string): Promise { if (rawIconCache.has(name)) { return rawIconCache.get(name)! } const icon = await loadIcon(name) if (!icon) { return null } rawIconCache.set(name, icon) return icon } /** * Load icon from Iconify and return SVG string * @param name Icon name in format "prefix:name" (e.g., "mdi:home", "lucide:star") * @param size Optional size (default: 24) */ export async function loadIconSvg(name: string, size: number = 24): Promise { const cacheKey = `${name}@${size}` if (iconCache.has(cacheKey)) { return iconCache.get(cacheKey)! } const icon = await loadRawIcon(name) if (!icon) { return null } const result = iconToSVG(icon, { height: size, width: size }) const svg = ` `${k}="${v}"`).join(' ')}>${result.body}` const data: IconData = { svg, width: size, height: size, body: result.body, viewBox: { left: result.viewBox[0], top: result.viewBox[1], width: result.viewBox[2], height: result.viewBox[3] } } iconCache.set(cacheKey, data) return data } /** * Get cached icon data (synchronous, for use in React components) * Returns null if icon not preloaded */ export function getIconData(name: string, size: number = 24): IconData | null { return iconCache.get(`${name}@${size}`) || null } /** * Preload icons for use in JSX render * Call before rendering to ensure icons are available synchronously */ export async function preloadIcons(icons: Array<{ name: string; size?: number }>): Promise { await Promise.all(icons.map(({ name, size }) => loadIconSvg(name, size || 24))) } /** * Get list of popular icon sets */ export const iconSets = { mdi: 'Material Design Icons', lucide: 'Lucide', heroicons: 'Heroicons', 'heroicons-outline': 'Heroicons Outline', 'heroicons-solid': 'Heroicons Solid', tabler: 'Tabler Icons', 'fa-solid': 'Font Awesome Solid', 'fa-regular': 'Font Awesome Regular', 'fa-brands': 'Font Awesome Brands', ri: 'Remix Icon', ph: 'Phosphor', 'ph-bold': 'Phosphor Bold', 'ph-fill': 'Phosphor Fill', carbon: 'Carbon', fluent: 'Fluent UI', ion: 'Ionicons', bi: 'Bootstrap Icons' } /** * Recursively collect Icon primitives from React element tree */ export function collectIcons(element: ReactElement): Array<{ name: string; size?: number }> { const icons: Array<{ name: string; size?: number }> = [] function traverse(node: ReactNode): void { if (!node || typeof node !== 'object') return if (Array.isArray(node)) { node.forEach(traverse) return } const el = node as ReactElement if (!el.type) return if (el.type === 'icon') { const props = el.props as { icon?: string; size?: number } if (props.icon) { icons.push({ name: props.icon, size: props.size }) } } if (typeof el.type === 'function') { try { const Component = el.type as (props: Record) => ReactNode const rendered = Component(el.props as Record) if (rendered) traverse(rendered) } catch { // Ignore render errors during collection } } const props = el.props as { children?: ReactNode } if (props.children) { if (Array.isArray(props.children)) { props.children.forEach(traverse) } else { traverse(props.children) } } } traverse(element) return icons }