import { DeesElement, html, property, customElement, cssManager, css, type TemplateResult, } from '@design.estate/dees-element'; import * as lucideIcons from 'lucide'; import { createElement } from 'lucide'; declare global { interface HTMLElementTagNameMap { 'sio-icon': SioIcon; } } @customElement('sio-icon') export class SioIcon extends DeesElement { public static demo = () => html`
`; @property({ type: String }) public icon: string; @property({ type: Number }) public size: number = 24; @property({ type: String }) public color: string = 'currentColor'; @property({ type: Number }) public strokeWidth: number = 2; // Cache for rendered icons private static iconCache = new Map(); private static readonly MAX_CACHE_SIZE = 100; // Track last rendered properties to avoid unnecessary updates private lastIcon: string | null = null; private lastSize: number | null = null; private lastColor: string | null = null; private lastStrokeWidth: number | null = null; public static styles = [ cssManager.defaultStyles, css` :host { display: inline-flex; align-items: center; justify-content: center; line-height: 1; vertical-align: middle; } #iconContainer { display: flex; align-items: center; justify-content: center; } #iconContainer svg { display: block; width: 100%; height: 100%; } `, ]; public render(): TemplateResult { return html`
`; } public updated() { // Check if we need to update if ( this.lastIcon === this.icon && this.lastSize === this.size && this.lastColor === this.color && this.lastStrokeWidth === this.strokeWidth ) { return; } // Update tracking properties this.lastIcon = this.icon; this.lastSize = this.size; this.lastColor = this.color; this.lastStrokeWidth = this.strokeWidth; const container = this.shadowRoot?.querySelector('#iconContainer') as HTMLElement; if (!container || !this.icon) return; // Clear container container.innerHTML = ''; // Create cache key const cacheKey = `${this.icon}:${this.size}:${this.color}:${this.strokeWidth}`; // Check cache if (SioIcon.iconCache.has(cacheKey)) { container.innerHTML = SioIcon.iconCache.get(cacheKey)!; return; } try { // Convert icon name to PascalCase (e.g., 'message-square' -> 'MessageSquare') const pascalCaseName = this.icon .split('-') .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); const iconComponent = (lucideIcons as any)[pascalCaseName]; if (!iconComponent) { console.warn(`Lucide icon '${pascalCaseName}' not found`); return; } // Create the icon element const svgElement = createElement(iconComponent, { size: this.size, color: this.color, strokeWidth: this.strokeWidth, }); if (svgElement) { // Cache the result const svgString = svgElement.outerHTML; SioIcon.iconCache.set(cacheKey, svgString); // Limit cache size if (SioIcon.iconCache.size > SioIcon.MAX_CACHE_SIZE) { const firstKey = SioIcon.iconCache.keys().next().value; SioIcon.iconCache.delete(firstKey); } // Append to container container.appendChild(svgElement); } } catch (error) { console.error(`Error rendering icon ${this.icon}:`, error); } } public async disconnectedCallback() { await super.disconnectedCallback(); // Clear references this.lastIcon = null; this.lastSize = null; this.lastColor = null; this.lastStrokeWidth = null; } }