diff --git a/package.json b/package.json
index 0c90264..2ef4828 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"apexcharts": "^4.3.0",
"highlight.js": "11.11.1",
"ibantools": "^4.5.1",
+ "lucide": "^0.488.0",
"monaco-editor": "^0.52.2",
"pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 34f32af..7852952 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -53,6 +53,9 @@ importers:
ibantools:
specifier: ^4.5.1
version: 4.5.1
+ lucide:
+ specifier: ^0.488.0
+ version: 0.488.0
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
@@ -3092,6 +3095,9 @@ packages:
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
engines: {node: '>=16.14'}
+ lucide@0.488.0:
+ resolution: {integrity: sha512-AcCuN/R9ZRDEH+H47azM5RWbhUTi02+OL4QHeY6kP/MGMSislGWtRDNqj/2EJIBp/qMKpuAU1v5BLAVT0BDE/g==}
+
make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
@@ -8640,6 +8646,8 @@ snapshots:
lru-cache@8.0.5: {}
+ lucide@0.488.0: {}
+
make-dir@3.1.0:
dependencies:
semver: 6.3.1
diff --git a/ts_web/elements/dees-icon.demo.ts b/ts_web/elements/dees-icon.demo.ts
index c58db3c..6876b0a 100644
--- a/ts_web/elements/dees-icon.demo.ts
+++ b/ts_web/elements/dees-icon.demo.ts
@@ -1,31 +1,159 @@
import { html } from '@design.estate/dees-element';
+import { icons, type IconWithPrefix } from './dees-icon.js';
+import * as lucideIcons from 'lucide';
-import { faIcons } from './dees-icon.js';
+export const demoFunc = () => {
+ // Group FontAwesome icons by type
+ const faIcons = Object.keys(icons.fa);
+
+ // Extract Lucide icons from the lucideIcons object directly
+ // Log the first few keys to understand the structure
+ console.log('First few Lucide keys:', Object.keys(lucideIcons).slice(0, 5));
+
+ // Get all icon functions from lucideIcons (they have PascalCase names)
+ const lucideIconsList = Object.keys(lucideIcons)
+ .filter(key => {
+ // Skip utility functions and focus on icon components (first letter is uppercase)
+ const isUppercaseFirst = key[0] === key[0].toUpperCase() && key[0] !== key[0].toLowerCase();
+ const isFunction = typeof lucideIcons[key] === 'function';
+ const notUtility = !['createElement', 'createIcons', 'default'].includes(key);
+ return isFunction && isUppercaseFirst && notUtility;
+ })
+ .map(pascalName => {
+ // Convert PascalCase to camelCase
+ return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
+ });
+
+ // Log how many icons we found
+ console.log(`Found ${lucideIconsList.length} Lucide icons`);
+
+ // If we didn't find any, try an alternative approach
+ if (lucideIconsList.length === 0) {
+ console.log('Trying alternative approach to find Lucide icons');
+
+ // Try to get icon names from a known property if available
+ if (lucideIcons.icons) {
+ const iconSource = lucideIcons.icons || {};
+ lucideIconsList.push(...Object.keys(iconSource));
+ console.log(`Found ${lucideIconsList.length} icons via alternative method`);
+ }
+ }
-export const demoFunc = () => html`
+ // Define the functions in TS scope instead of script tags
+ const searchIcons = (event: InputEvent) => {
+ const searchTerm = (event.target as HTMLInputElement).value.toLowerCase().trim();
+ const containers = document.querySelectorAll('.iconContainer');
+
+ containers.forEach(container => {
+ const iconName = container.getAttribute('data-name');
+
+ // If search term is empty, show all icons
+ if (searchTerm === '') {
+ container.classList.remove('hidden');
+ } else if (iconName && iconName.includes(searchTerm)) {
+ container.classList.remove('hidden');
+ } else {
+ container.classList.add('hidden');
+ }
+ });
+
+ // Update counts
+ document.querySelectorAll('.section-container').forEach(section => {
+ const visibleIcons = section.querySelectorAll('.iconContainer:not(.hidden)').length;
+ const countElement = section.querySelector('.icon-count');
+ if (countElement) {
+ const totalIconsCount = section.classList.contains('fa-section')
+ ? faIcons.length
+ : lucideIconsList.length;
+
+ countElement.textContent = visibleIcons === totalIconsCount
+ ? `${totalIconsCount} icons`
+ : `${visibleIcons} of ${totalIconsCount} icons`;
+ }
+ });
+ };
+
+ const copyIconName = (iconNameToCopy: string, type: 'fa' | 'lucide') => {
+ // Use the new prefix format
+ const textToCopy = `${type}:${iconNameToCopy}`;
+
+ navigator.clipboard.writeText(textToCopy).then(() => {
+ // Find the event target
+ const currentEvent = window.event as MouseEvent;
+ const currentTarget = currentEvent.currentTarget as HTMLElement;
+ // Show feedback
+ const tooltip = currentTarget.querySelector('.copy-tooltip');
+ if (tooltip) {
+ tooltip.textContent = 'Copied!';
+
+ setTimeout(() => {
+ tooltip.textContent = 'Click to copy';
+ }, 2000);
+ }
+ });
+ };
+
+ return html`
-
- ${Object.keys(faIcons).map(
- (iconName) => html`
-
- `
- )}
+
+
+
-
-`;
+
+
+ New API: Use icon="fa:iconName"
or icon="lucide:iconName"
instead of iconFA
.
+ Click any icon to copy its new format to clipboard.
+
+
+
+
+ FontAwesome Icons
+ ${faIcons.length} icons
+
+
+ ${faIcons.map(
+ (iconName) => {
+ const prefixedName = `fa:${iconName}`;
+ return html`
+
copyIconName(iconName, 'fa')}>
+
+
${iconName}
+
Click to copy
+
+ `;
+ }
+ )}
+
+
+
+
+
+ Lucide Icons
+ ${lucideIconsList.length} icons
+
+
+ ${lucideIconsList.map(
+ (iconName) => {
+ const prefixedName = `lucide:${iconName}`;
+ return html`
+
copyIconName(iconName, 'lucide')}>
+
+
${iconName}
+
Click to copy
+
+ `;
+ }
+ )}
+
+
+
+ `;
+};
\ No newline at end of file
diff --git a/ts_web/elements/dees-icon.ts b/ts_web/elements/dees-icon.ts
index 6417e5b..e32b444 100644
--- a/ts_web/elements/dees-icon.ts
+++ b/ts_web/elements/dees-icon.ts
@@ -75,7 +75,12 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { demoFunc } from './dees-icon.demo.js';
-export const faIcons = {
+// Import Lucide icons and the createElement function
+import * as lucideIcons from 'lucide';
+import { createElement } from 'lucide';
+
+// Collect FontAwesome icons
+const faIcons = {
// normal
arrowRight: faArrowRightSolid,
arrowUpRightFromSquare: faArrowUpRightFromSquareSolid,
@@ -136,7 +141,32 @@ export const faIcons = {
twitter: faTwitter,
};
-export type TIconKey = keyof typeof faIcons;
+// Create a string literal type for all FA icons
+type FAIconKey = keyof typeof faIcons;
+
+// Create union types for the icons with prefixes
+export type IconWithPrefix = `fa:${FAIconKey}` | `lucide:${string}`;
+
+// Export only FontAwesome icons directly
+export const icons = {
+ fa: faIcons
+};
+
+// Legacy type for backward compatibility
+export type TIconKey = FAIconKey | `lucide:${string}`;
+
+// Use a global static cache for all icons to reduce rendering
+const iconCache = new Map
();
+
+// Clear cache items occasionally to prevent memory leaks
+const MAX_CACHE_SIZE = 500;
+function limitCacheSize() {
+ if (iconCache.size > MAX_CACHE_SIZE) {
+ // Remove oldest entries (first 20% of items)
+ const keysToDelete = Array.from(iconCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
+ keysToDelete.forEach(key => iconCache.delete(key));
+ }
+}
declare global {
interface HTMLElementTagNameMap {
@@ -148,31 +178,170 @@ declare global {
export class DeesIcon extends DeesElement {
public static demo = demoFunc;
+ /**
+ * @deprecated Use the `icon` property instead with format "fa:iconName" or "lucide:iconName"
+ */
@property({
- type: String
+ type: String,
+ converter: {
+ // Convert attribute string to property (for reflected attributes)
+ fromAttribute: (value: string): TIconKey => value as TIconKey,
+ // Convert property to attribute (for reflection)
+ toAttribute: (value: TIconKey): string => value
+ }
})
- public iconFA: keyof typeof faIcons;
+ public iconFA?: TIconKey;
- @property()
+ /**
+ * The preferred icon property. Use format "fa:iconName" or "lucide:iconName"
+ * Examples: "fa:check", "lucide:menu"
+ */
+ @property({
+ type: String,
+ converter: {
+ fromAttribute: (value: string): IconWithPrefix => value as IconWithPrefix,
+ toAttribute: (value: IconWithPrefix): string => value
+ }
+ })
+ public icon?: IconWithPrefix;
+
+ @property({ type: Number })
public iconSize: number;
+ @property({ type: String })
+ public color: string = 'currentColor';
+
+ @property({ type: Number })
+ public strokeWidth: number = 2;
+
+ // For tracking when we need to re-render
+ private lastIcon: IconWithPrefix | TIconKey | null = null;
+ private lastIconSize: number | null = null;
+ private lastColor: string | null = null;
+ private lastStrokeWidth: number | null = null;
+
constructor() {
super();
domtools.elementBasic.setup();
}
+ /**
+ * Gets the effective icon value, supporting both the new `icon` property
+ * and the legacy `iconFA` property for backward compatibility.
+ * Prefers `icon` if both are set.
+ */
+ private getEffectiveIcon(): IconWithPrefix | TIconKey | null {
+ // Prefer the new API
+ if (this.icon) {
+ return this.icon;
+ }
+
+ // Fall back to the old API
+ if (this.iconFA) {
+ // If iconFA is already in the proper format (lucide:name), use it directly
+ if (this.iconFA.startsWith('lucide:')) {
+ return this.iconFA;
+ }
+
+ // For FontAwesome icons with no prefix, add the prefix
+ return `fa:${this.iconFA}` as IconWithPrefix;
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses an icon string into its type and name parts
+ * @param iconStr The icon string in format "type:name"
+ * @returns Object with type and name properties
+ */
+ private parseIconString(iconStr: string): { type: 'fa' | 'lucide', name: string } {
+ if (iconStr.startsWith('fa:')) {
+ return {
+ type: 'fa',
+ name: iconStr.substring(3) // Remove 'fa:' prefix
+ };
+ } else if (iconStr.startsWith('lucide:')) {
+ return {
+ type: 'lucide',
+ name: iconStr.substring(7) // Remove 'lucide:' prefix
+ };
+ } else {
+ // For backward compatibility, assume FontAwesome if no prefix
+ return {
+ type: 'fa',
+ name: iconStr
+ };
+ }
+ }
+
+ private renderLucideIcon(iconName: string): string {
+ // Create a cache key based on all visual properties
+ const cacheKey = `lucide:${iconName}:${this.iconSize}:${this.color}:${this.strokeWidth}`;
+
+ // Check if we already have this icon in the cache
+ if (iconCache.has(cacheKey)) {
+ return iconCache.get(cacheKey) || '';
+ }
+
+ try {
+ // Get the Pascal case icon name (Menu instead of menu)
+ const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1);
+
+ // Check if the icon exists in lucideIcons
+ if (!lucideIcons[pascalCaseName]) {
+ console.warn(`Lucide icon '${pascalCaseName}' not found in lucideIcons object`);
+ return '';
+ }
+
+ // Use the exact pattern from Lucide documentation
+ const svgElement = createElement(lucideIcons[pascalCaseName], {
+ color: this.color,
+ size: this.iconSize,
+ strokeWidth: this.strokeWidth
+ });
+
+ if (!svgElement) {
+ console.warn(`createElement returned empty result for ${pascalCaseName}`);
+ return '';
+ }
+
+ // Get the HTML
+ const result = svgElement.outerHTML;
+
+ // Cache the result for future use
+ iconCache.set(cacheKey, result);
+ limitCacheSize();
+
+ return result;
+ } catch (error) {
+ console.error(`Error rendering Lucide icon ${iconName}:`, error);
+
+ // Create a fallback SVG with the icon name
+ return ``;
+ }
+ }
+
public static styles = [
cssManager.defaultStyles,
css`
:host {
- display: block;
- white-space: nowrap;
- display: flex;
+ display: inline-flex;
align-items: center;
justify-content: center;
+ line-height: 1;
+ vertical-align: middle;
}
- * {
- transition: inherit !important;
+
+ /* Improve rendering performance */
+ #iconContainer svg {
+ display: block;
+ height: 100%;
+ width: 100%;
+ will-change: transform; /* Helps with animations */
+ contain: strict; /* Performance optimization */
}
`,
];
@@ -181,8 +350,8 @@ export class DeesIcon extends DeesElement {
return html`
${domtools.elementBasic.styles}
@@ -190,14 +359,95 @@ export class DeesIcon extends DeesElement {
`;
}
- public async updated() {
+ public updated() {
+ // If size is not specified, use font size as a base
if (!this.iconSize) {
this.iconSize = parseInt(globalThis.getComputedStyle(this).fontSize.replace(/\D/g,''));
}
- if (this.iconFA) {
- this.shadowRoot.querySelector('#iconContainer').innerHTML = this.iconFA
- ? icon(faIcons[this.iconFA]).html[0]
- : 'icon not found';
+
+ // Get the effective icon (either from icon or iconFA property)
+ const effectiveIcon = this.getEffectiveIcon();
+
+ // Check if we actually need to update the icon
+ // This prevents unnecessary DOM operations when properties haven't changed
+ if (this.lastIcon === effectiveIcon &&
+ this.lastIconSize === this.iconSize &&
+ this.lastColor === this.color &&
+ this.lastStrokeWidth === this.strokeWidth) {
+ return; // No visual changes - skip update
+ }
+
+ // Update our "last properties" for future change detection
+ this.lastIcon = effectiveIcon;
+ this.lastIconSize = this.iconSize;
+ this.lastColor = this.color;
+ this.lastStrokeWidth = this.strokeWidth;
+
+ const container = this.shadowRoot?.querySelector('#iconContainer');
+ if (!container || !effectiveIcon) return;
+
+ try {
+ // Parse the icon string to get type and name
+ const { type, name } = this.parseIconString(effectiveIcon);
+
+ if (type === 'lucide') {
+ // For Lucide, use direct DOM manipulation as shown in the docs
+ // This approach avoids HTML string issues
+ container.innerHTML = ''; // Clear container
+
+ try {
+ // Convert to PascalCase
+ const pascalCaseName = name.charAt(0).toUpperCase() + name.slice(1);
+
+ if (lucideIcons[pascalCaseName]) {
+ // Use the documented pattern from Lucide docs
+ const svgElement = createElement(lucideIcons[pascalCaseName], {
+ color: this.color,
+ size: this.iconSize,
+ strokeWidth: this.strokeWidth
+ });
+
+ if (svgElement) {
+ // Directly append the element
+ container.appendChild(svgElement);
+ return; // Exit early since we've added the element
+ }
+ }
+
+ // If we reach here, something went wrong
+ throw new Error(`Could not create element for ${pascalCaseName}`);
+ } catch (error) {
+ console.error(`Error rendering Lucide icon:`, error);
+
+ // Fall back to the string-based approach
+ const iconHtml = this.renderLucideIcon(name);
+ if (iconHtml) {
+ container.innerHTML = iconHtml;
+ }
+ }
+ } else {
+ // Use FontAwesome rendering via HTML string
+ const faIcon = icons.fa[name as FAIconKey];
+ if (faIcon) {
+ const iconHtml = icon(faIcon).html[0];
+ container.innerHTML = iconHtml;
+ } else {
+ console.warn(`FontAwesome icon not found: ${name}`);
+ }
+ }
+ } catch (error) {
+ console.error(`Error updating icon ${effectiveIcon}:`, error);
}
}
-}
+
+ // Clean up resources when element is removed
+ async disconnectedCallback() {
+ super.disconnectedCallback();
+
+ // Clear our references
+ this.lastIcon = null;
+ this.lastIconSize = null;
+ this.lastColor = null;
+ this.lastStrokeWidth = null;
+ }
+}
\ No newline at end of file