190 lines
4.7 KiB
TypeScript
190 lines
4.7 KiB
TypeScript
|
|
import {
|
||
|
|
DeesElement,
|
||
|
|
css,
|
||
|
|
cssManager,
|
||
|
|
customElement,
|
||
|
|
html,
|
||
|
|
property,
|
||
|
|
type TemplateResult,
|
||
|
|
} from '@design.estate/dees-element';
|
||
|
|
|
||
|
|
import * as lucideIcons from 'lucide';
|
||
|
|
import { createElement } from 'lucide';
|
||
|
|
import { demoFunc } from './dees-mobile-icon.demo.js';
|
||
|
|
|
||
|
|
// Create a type-safe icon name type
|
||
|
|
export type LucideIconName = keyof typeof lucideIcons;
|
||
|
|
|
||
|
|
// Cache for rendered icons to improve performance
|
||
|
|
const iconCache = new Map<string, string>();
|
||
|
|
const MAX_CACHE_SIZE = 500;
|
||
|
|
|
||
|
|
function limitCacheSize() {
|
||
|
|
if (iconCache.size > MAX_CACHE_SIZE) {
|
||
|
|
const keysToDelete = Array.from(iconCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
|
||
|
|
keysToDelete.forEach(key => iconCache.delete(key));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
declare global {
|
||
|
|
interface HTMLElementTagNameMap {
|
||
|
|
'dees-mobile-icon': DeesMobileIcon;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@customElement('dees-mobile-icon')
|
||
|
|
export class DeesMobileIcon extends DeesElement {
|
||
|
|
public static demo = demoFunc;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor icon: string = '';
|
||
|
|
|
||
|
|
@property({ type: Number })
|
||
|
|
accessor size: number = 20;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor color: string = 'currentColor';
|
||
|
|
|
||
|
|
@property({ type: Number })
|
||
|
|
accessor strokeWidth: number = 2;
|
||
|
|
|
||
|
|
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;
|
||
|
|
height: 100%;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
];
|
||
|
|
|
||
|
|
private renderLucideIcon(iconName: string): string {
|
||
|
|
const cacheKey = `${iconName}:${this.size}:${this.color}:${this.strokeWidth}`;
|
||
|
|
|
||
|
|
if (iconCache.has(cacheKey)) {
|
||
|
|
return iconCache.get(cacheKey) || '';
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Convert kebab-case to PascalCase (e.g., "chevron-down" -> "ChevronDown")
|
||
|
|
const pascalCaseName = iconName
|
||
|
|
.split('-')
|
||
|
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||
|
|
.join('');
|
||
|
|
|
||
|
|
if (!(lucideIcons as any)[pascalCaseName]) {
|
||
|
|
console.warn(`Lucide icon '${pascalCaseName}' not found`);
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
|
||
|
|
color: this.color,
|
||
|
|
size: this.size,
|
||
|
|
strokeWidth: this.strokeWidth
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!svgElement) {
|
||
|
|
console.warn(`createElement returned empty result for ${pascalCaseName}`);
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = svgElement.outerHTML;
|
||
|
|
iconCache.set(cacheKey, result);
|
||
|
|
limitCacheSize();
|
||
|
|
|
||
|
|
return result;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Error rendering Lucide icon ${iconName}:`, error);
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public render(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<style>
|
||
|
|
#iconContainer {
|
||
|
|
width: ${this.size}px;
|
||
|
|
height: ${this.size}px;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
<div id="iconContainer"></div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
updated() {
|
||
|
|
// Check if we actually need to update the icon
|
||
|
|
if (this.lastIcon === this.icon &&
|
||
|
|
this.lastSize === this.size &&
|
||
|
|
this.lastColor === this.color &&
|
||
|
|
this.lastStrokeWidth === this.strokeWidth) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.lastIcon = this.icon || null;
|
||
|
|
this.lastSize = this.size;
|
||
|
|
this.lastColor = this.color;
|
||
|
|
this.lastStrokeWidth = this.strokeWidth;
|
||
|
|
|
||
|
|
const container = this.shadowRoot?.querySelector('#iconContainer');
|
||
|
|
if (!container || !this.icon) return;
|
||
|
|
|
||
|
|
container.innerHTML = '';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const pascalCaseName = this.icon
|
||
|
|
.split('-')
|
||
|
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||
|
|
.join('');
|
||
|
|
|
||
|
|
if ((lucideIcons as any)[pascalCaseName]) {
|
||
|
|
const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
|
||
|
|
color: this.color,
|
||
|
|
size: this.size,
|
||
|
|
strokeWidth: this.strokeWidth
|
||
|
|
});
|
||
|
|
|
||
|
|
if (svgElement) {
|
||
|
|
container.appendChild(svgElement);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fall back to string-based approach
|
||
|
|
const iconHtml = this.renderLucideIcon(this.icon);
|
||
|
|
if (iconHtml) {
|
||
|
|
container.innerHTML = iconHtml;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Error updating icon ${this.icon}:`, error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async disconnectedCallback() {
|
||
|
|
await super.disconnectedCallback();
|
||
|
|
this.lastIcon = null;
|
||
|
|
this.lastSize = null;
|
||
|
|
this.lastColor = null;
|
||
|
|
this.lastStrokeWidth = null;
|
||
|
|
}
|
||
|
|
}
|