diff --git a/.eslintignore b/.eslintignore index ffae92223..afce5388f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,4 @@ node_modules docs/images docs/**/examples/ packages/lucide-react/dynamicIconImports.js +packages/angular/.angular diff --git a/packages/angular/scripts/exportTemplate.mts b/packages/angular/scripts/exportTemplate.mts index 3ea7f9bde..a7a7bd3ea 100644 --- a/packages/angular/scripts/exportTemplate.mts +++ b/packages/angular/scripts/exportTemplate.mts @@ -16,10 +16,10 @@ export default defineExportTemplate(async ({ const angularComponentName = `Lucide${componentName}`; const selectors = [`svg[lucide${toPascalCase(iconName)}]`]; const aliasComponentNames: string[] = []; - for (const alias of aliases) { - const aliasName = typeof alias === 'string' ? alias : alias.name; - const aliasComponentName = `Lucide${toPascalCase(aliasName)}`; - const aliasSelector = `svg[lucide${toPascalCase(aliasName)}]`; + const aliasNames = aliases.map(alias => typeof alias === 'string' ? alias : alias.name); + for (const alias of aliasNames) { + const aliasComponentName = `Lucide${toPascalCase(alias)}`; + const aliasSelector = `svg[lucide${toPascalCase(alias)}]`; if (!selectors.includes(aliasSelector)) { selectors.push(aliasSelector); } @@ -49,10 +49,12 @@ import { Component, signal } from '@angular/core'; standalone: true, }) export class ${angularComponentName} extends LucideIconBase { - static readonly iconName = '${iconName}'; - static readonly iconData: LucideIconData = ${JSON.stringify(children)}; - protected override readonly iconName = signal(${angularComponentName}.iconName); - protected override readonly iconData = signal(${angularComponentName}.iconData); + static readonly icon: LucideIconData = ${JSON.stringify({ + name: iconName, + node: children, + ...(aliasNames.length > 0 && { aliases: aliasNames }), + })}; + protected override readonly icon = signal(${angularComponentName}.icon); } ${aliasComponentNames.map((aliasComponentName) => { diff --git a/packages/angular/src/lucide-icon.spec.ts b/packages/angular/src/lucide-dynamic-icon.spec.ts similarity index 63% rename from packages/angular/src/lucide-icon.spec.ts rename to packages/angular/src/lucide-dynamic-icon.spec.ts index e6f2652f9..3a1ee7229 100644 --- a/packages/angular/src/lucide-icon.spec.ts +++ b/packages/angular/src/lucide-dynamic-icon.spec.ts @@ -1,5 +1,6 @@ import { Component, input, inputBinding, signal, WritableSignal } from '@angular/core'; -import { LucideIcon } from './lucide-icon'; +import { LucideDynamicIcon } from './lucide-dynamic-icon'; +import { provideLucideConfig } from './lucide-config'; import { LucideIconData, LucideIconInput } from './types'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideLucideIcons } from './lucide-icons'; @@ -12,44 +13,39 @@ import { By } from '@angular/platform-browser'; }`, - imports: [LucideIcon], + imports: [LucideDynamicIcon], }) class TestHostComponent { readonly icon = input(); } -describe('LucideIcon', () => { - let component: LucideIcon; - let fixture: ComponentFixture; +describe('LucideDynamicIcon', () => { + let component: LucideDynamicIcon; + let fixture: ComponentFixture; let icon: WritableSignal; - let name: WritableSignal; let title: WritableSignal; let color: WritableSignal; let size: WritableSignal; let strokeWidth: WritableSignal; let absoluteStrokeWidth: WritableSignal; const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr); - const testIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]]; - const testIcon2: LucideIconData = [ - ['circle', { cx: 12, cy: 12, r: 8 }], - ['polyline', { points: '1 1 22 22' }], - ]; - beforeEach(async () => { - TestBed.configureTestingModule({ - providers: [provideLucideIcons({ demo: testIcon })], - }); - icon = signal('demo'); - name = signal(undefined); - title = signal(undefined); - color = signal(undefined); - size = signal(undefined); - strokeWidth = signal(undefined); - absoluteStrokeWidth = signal(undefined); - fixture = TestBed.createComponent(LucideIcon, { + const testIcon: LucideIconData = { + name: 'demo', + node: [['polyline', { points: '1 1 22 22' }]], + }; + const testIcon2: LucideIconData = { + name: 'demo-other', + node: [ + ['circle', { cx: 12, cy: 12, r: 8 }], + ['polyline', { points: '1 1 22 22' }], + ], + aliases: ['demo-2'], + }; + function createComponent() { + return TestBed.createComponent(LucideDynamicIcon, { inferTagName: true, bindings: [ inputBinding('lucideIcon', icon), - inputBinding('name', name), inputBinding('title', title), inputBinding('color', color), inputBinding('size', size), @@ -57,6 +53,18 @@ describe('LucideIcon', () => { inputBinding('absoluteStrokeWidth', absoluteStrokeWidth), ], }); + } + beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [provideLucideIcons(testIcon)], + }); + icon = signal('demo'); + title = signal(undefined); + color = signal(undefined); + size = signal(undefined); + strokeWidth = signal(undefined); + absoluteStrokeWidth = signal(undefined); + fixture = createComponent(); component = fixture.componentInstance; }); @@ -82,30 +90,23 @@ describe('LucideIcon', () => { describe('iconInput', () => { it('should support LucideIconData input', () => { icon.set(testIcon); - name.set('custom-name'); fixture.detectChanges(); - - expect(component['iconData']()).toBe(testIcon); - expect(component['iconName']()).toBe('custom-name'); + expect(component['icon']()).toBe(testIcon); expect(fixture.nativeElement.innerHTML).toBe( '', ); }); - it('should support LucideIconComponentType input', () => { + it('should support LucideIcon input', () => { icon.set(LucideActivity); fixture.detectChanges(); - - expect(component['iconData']()).toBe(LucideActivity.iconData); - expect(component['iconName']()).toBe(LucideActivity.iconName); + expect(component['icon']()).toBe(LucideActivity.icon); }); it('should support string icon name', () => { icon.set('demo'); fixture.detectChanges(); - - expect(component['iconData']()).toBe(testIcon); - expect(component['iconName']()).toBe('demo'); + expect(component['icon']()).toBe(testIcon); }); - it('should throw error if no icon founds', () => { + it('should throw error if no icon found', () => { icon.set('invalid'); expect(() => fixture.detectChanges()).toThrowError(`Unable to resolve icon 'invalid'`); }); @@ -116,12 +117,10 @@ describe('LucideIcon', () => { fixture.detectChanges(); expect(getSvgAttribute('class')).toBe('lucide lucide-demo'); }); - it('should add class from name, even if icon has name', () => { - icon.set(LucideActivity); - name.set('custom-name'); + it('should add backwards compatible classes from aliases', () => { + icon.set(testIcon2); fixture.detectChanges(); - - expect(getSvgAttribute('class')).toBe('lucide lucide-custom-name'); + expect(getSvgAttribute('class')).toBe('lucide lucide-demo-other lucide-demo-2'); }); it('should add class icon if available', () => { icon.set(LucideActivity); @@ -195,16 +194,21 @@ describe('LucideIcon', () => { it('should not adjust stroke width', () => { strokeWidth.set(2); size.set(12); - absoluteStrokeWidth.set(false); + absoluteStrokeWidth.set(true); fixture.detectChanges(); expect(getSvgAttribute('stroke-width')).toBe('2'); }); - it('should adjust stroke width', () => { - strokeWidth.set(2); - size.set(12); + it('should not set vector-effect on children', () => { + absoluteStrokeWidth.set(false); + for (const child of fixture.nativeElement.children) { + expect(child.getAttribute('vector-effect')).toBeNull(); + } + }); + it('should set vector-effect on children', () => { absoluteStrokeWidth.set(true); - fixture.detectChanges(); - expect(getSvgAttribute('stroke-width')).toBe('4'); + for (const child of fixture.nativeElement.children) { + expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); + } }); }); @@ -240,4 +244,72 @@ describe('LucideIcon', () => { expect(rect.outerHTML).toBe(''); }); }); + + describe('LUCIDE_CONFIG', () => { + beforeEach(async () => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideLucideIcons(testIcon), + provideLucideConfig({ + color: 'red', + strokeWidth: 1, + size: 12, + absoluteStrokeWidth: true, + }), + ], + }); + await TestBed.compileComponents(); + fixture = createComponent(); + component = fixture.componentInstance; + }); + describe('color', () => { + it('should use color from config', () => { + fixture.detectChanges(); + expect(getSvgAttribute('stroke')).toBe('red'); + }); + it('should use override color from config', () => { + color.set('pink'); + fixture.detectChanges(); + expect(getSvgAttribute('stroke')).toBe('pink'); + }); + }); + describe('strokeWidth', () => { + it('should use stroke width from config', () => { + fixture.detectChanges(); + expect(getSvgAttribute('stroke-width')).toBe('1'); + }); + it('should use override stroke width from config', () => { + strokeWidth.set(3); + fixture.detectChanges(); + expect(getSvgAttribute('stroke-width')).toBe('3'); + }); + }); + describe('size', () => { + it('should use size from config', () => { + fixture.detectChanges(); + expect(getSvgAttribute('width')).toBe('12'); + expect(getSvgAttribute('height')).toBe('12'); + }); + it('should use override size from config', () => { + size.set('48'); + fixture.detectChanges(); + expect(getSvgAttribute('width')).toBe('48'); + expect(getSvgAttribute('height')).toBe('48'); + }); + }); + describe('absoluteStrokeWidth', () => { + it('should use absoluteStrokeWidth from config', () => { + for (const child of fixture.nativeElement.children) { + expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); + } + }); + it('should override absoluteStrokeWidth', () => { + absoluteStrokeWidth.set(false); + for (const child of fixture.nativeElement.children) { + expect(child.getAttribute('vector-effect')).toBeNull(); + } + }); + }); + }); }); diff --git a/packages/angular/src/lucide-dynamic-icon.ts b/packages/angular/src/lucide-dynamic-icon.ts new file mode 100644 index 000000000..ce678672d --- /dev/null +++ b/packages/angular/src/lucide-dynamic-icon.ts @@ -0,0 +1,33 @@ +import { Component, computed, inject, input } from '@angular/core'; +import { isLucideIconComponent, isLucideIconData, LucideIconInput } from './types'; +import { LucideIconBase } from './lucide-icon-base'; +import { LUCIDE_ICONS } from './lucide-icons'; + +/** + * Generic icon component for rendering LucideIconData. + */ +@Component({ + selector: 'svg[lucideIcon]', + templateUrl: './lucide-icon.html', + standalone: true, +}) +export class LucideDynamicIcon extends LucideIconBase { + protected readonly icons = inject(LUCIDE_ICONS); + public readonly lucideIcon = input.required(); + + protected override readonly icon = computed(() => { + const icon = this.lucideIcon(); + if (isLucideIconData(icon)) { + return icon; + } else if (isLucideIconComponent(icon)) { + return icon.icon; + } else if (typeof icon === 'string') { + if (icon in this.icons) { + return this.icons[icon]; + } else { + throw new Error(`Unable to resolve icon '${icon}'`); + } + } + return null; + }); +} diff --git a/packages/angular/src/lucide-icon-base.ts b/packages/angular/src/lucide-icon-base.ts index 39c688a53..d02a2f3f0 100644 --- a/packages/angular/src/lucide-icon-base.ts +++ b/packages/angular/src/lucide-icon-base.ts @@ -11,8 +11,6 @@ import { import { LUCIDE_CONFIG } from './lucide-config'; import { LucideIconData, Nullable } from './types'; import defaultAttributes from './default-attributes'; -import { formatFixed } from './utils/format-fixed'; -import { toKebabCase } from './utils/to-kebab-case'; function transformNumericStringInput( value: Nullable, @@ -41,13 +39,12 @@ function transformNumericStringInput( '[attr.width]': 'size().toString(10)', '[attr.height]': 'size().toString(10)', '[attr.stroke]': 'color()', - '[attr.stroke-width]': 'computedStrokeWidth()', + '[attr.stroke-width]': 'strokeWidth().toString(10)', '[attr.aria-hidden]': '!title()', }, }) export abstract class LucideIconBase { - protected abstract readonly iconName: Signal>; - protected abstract readonly iconData: Signal>; + protected abstract readonly icon: Signal>; protected readonly iconConfig = inject(LUCIDE_CONFIG); protected readonly elRef = inject(ElementRef); protected readonly renderer = inject(Renderer2); @@ -88,33 +85,27 @@ export abstract class LucideIconBase { transformNumericStringInput(value, this.iconConfig.strokeWidth), }); /** - * Whether stroke width should be scaled to appear uniform regardless of icon size. - * - * @remarks - * Use CSS to set on SVG paths instead: - * ```css - * .lucide * { - * vector-effect: non-scaling-stroke; - * } - * ``` + * If set to true, it adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements. */ readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, { transform: (value: Nullable) => value ?? this.iconConfig.absoluteStrokeWidth, }); - protected readonly computedStrokeWidth = computed(() => { - const strokeWidth = this.strokeWidth(); - const size = this.size(); - return this.absoluteStrokeWidth() - ? formatFixed(strokeWidth / (size / 24)) - : strokeWidth.toString(10); - }); constructor() { effect((onCleanup) => { - const icon = this.iconData(); + const icon = this.icon(); if (icon) { - const elements = icon.map(([name, attrs]) => { + const absoluteStrokeWidth = this.absoluteStrokeWidth(); + const { name, node, aliases = [] } = icon; + const classes = [name, ...aliases].map((item) => `lucide-${item}`); + for (const cssClass of classes) { + this.renderer.addClass(this.elRef.nativeElement, cssClass); + } + const elements = node.map(([name, attrs]) => { const element = this.renderer.createElement(name, 'http://www.w3.org/2000/svg'); + if (absoluteStrokeWidth) { + this.renderer.setAttribute(element, 'vector-effect', 'non-scaling-stroke'); + } Object.entries(attrs).forEach(([name, value]) => this.renderer.setAttribute( element, @@ -129,16 +120,9 @@ export abstract class LucideIconBase { elements.forEach((element) => this.renderer.removeChild(this.elRef.nativeElement, element), ); - }); - } - }); - effect((onCleanup) => { - const name = this.iconName(); - if (name) { - const cssClass = `lucide-${toKebabCase(name)}`; - this.renderer.addClass(this.elRef.nativeElement, cssClass); - onCleanup(() => { - this.renderer.removeClass(this.elRef.nativeElement, cssClass); + for (const cssClass of classes) { + this.renderer.removeClass(this.elRef.nativeElement, cssClass); + } }); } }); diff --git a/packages/angular/src/lucide-icon.ts b/packages/angular/src/lucide-icon.ts deleted file mode 100644 index eb718f418..000000000 --- a/packages/angular/src/lucide-icon.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Component, computed, inject, input } from '@angular/core'; -import { isLucideIconComponent, isLucideIconData, LucideIconInput } from './types'; -import { LucideIconBase } from './lucide-icon-base'; -import { LUCIDE_ICONS } from './lucide-icons'; -import { LucideIconData } from './types'; -import { toKebabCase } from './utils/to-kebab-case'; - -interface LucideResolvedIcon { - name?: string | null; - data: LucideIconData; -} - -/** - * Generic icon component for rendering LucideIconData. - */ -@Component({ - selector: 'svg[lucideIcon]', - templateUrl: './lucide-icon.html', - standalone: true, -}) -export class LucideIcon extends LucideIconBase { - protected readonly icons = inject(LUCIDE_ICONS); - readonly name = input(); - readonly iconInput = input.required({ - alias: 'lucideIcon', - }); - readonly resolvedIcon = computed(() => { - return this.resolveIcon(this.name(), this.iconInput()); - }); - protected override readonly iconName = computed(() => { - return this.resolvedIcon()?.name; - }); - protected override readonly iconData = computed(() => { - return this.resolvedIcon()?.data; - }); - - protected resolveIcon( - name: string | null | undefined, - icon: LucideIconInput | null | undefined, - ): LucideResolvedIcon | null { - if (isLucideIconData(icon)) { - return { - name, - data: icon, - }; - } else if (isLucideIconComponent(icon)) { - return { - name: name ?? icon.iconName, - data: icon.iconData, - }; - } else if (typeof icon === 'string') { - const name = toKebabCase(icon); - if (name in this.icons) { - return { - name, - data: this.icons[name], - }; - } else { - throw new Error(`Unable to resolve icon '${icon}'`); - } - } - - return null; - } -} diff --git a/packages/angular/src/lucide-icons.spec.ts b/packages/angular/src/lucide-icons.spec.ts index 063867ed9..dba1f358d 100644 --- a/packages/angular/src/lucide-icons.spec.ts +++ b/packages/angular/src/lucide-icons.spec.ts @@ -1,5 +1,10 @@ import { TestBed } from '@angular/core/testing'; -import { LUCIDE_ICONS, provideLucideIcons } from './lucide-icons'; +import { + LUCIDE_ICONS, + lucideLegacyIcon, + lucideLegacyIconMap, + provideLucideIcons, +} from './lucide-icons'; import { LucideIconData } from './types'; import { LucideActivity } from './icons/activity'; import { LucideCircle } from './icons/circle'; @@ -12,32 +17,49 @@ describe('Lucide icons', () => { }); }); describe('provideLucideIcons', () => { - const mockIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]]; - const mockIcon2: LucideIconData = [['circle', { cx: 12, cy: 12, r: 8 }]]; - it('should accept dictionary of icons', () => { + const mockIcon: LucideIconData = { + name: 'mock-icon', + node: [['polyline', { points: '1 1 22 22' }]], + }; + const mockIcon2: LucideIconData = { + name: 'mock-icon-circle', + node: [['circle', { cx: 12, cy: 12, r: 8 }]], + aliases: ['mock-icon-2'], + }; + const legacyIconNode: LucideIconData['node'] = [['circle', { cx: 12, cy: 12, r: 8 }]]; + const legacyAlias = 'legacy-old-name'; + const OtherLegacyIcon = legacyIconNode; + it('should accept list of icon object, icon components or legacy icons', () => { TestBed.configureTestingModule({ providers: [ - provideLucideIcons({ - DemoIcon: mockIcon, - MockIcon: mockIcon2, - TestIcon: LucideActivity, - }), + provideLucideIcons( + mockIcon, + mockIcon2, + LucideCircle, + lucideLegacyIcon('legacy-icon', legacyIconNode, [legacyAlias]), + ...lucideLegacyIconMap({ OtherLegacyIcon }), + ), ], }); + const legacyIconData = { + name: 'legacy-icon', + node: legacyIconNode, + aliases: [legacyAlias], + }; + const otherLegacyIconData = { + name: 'other-legacy-icon', + node: legacyIconNode, + aliases: ['OtherLegacyIcon'], + }; expect(TestBed.inject(LUCIDE_ICONS)).toEqual({ - 'demo-icon': mockIcon, - 'mock-icon': mockIcon2, - [LucideActivity.iconName]: LucideActivity.iconData, - }); - }); - it('should accept list of icon components', () => { - TestBed.configureTestingModule({ - providers: [provideLucideIcons([LucideActivity, LucideSquareX, LucideCircle])], - }); - expect(TestBed.inject(LUCIDE_ICONS)).toEqual({ - [LucideActivity.iconName]: LucideActivity.iconData, - [LucideSquareX.iconName]: LucideSquareX.iconData, - [LucideCircle.iconName]: LucideCircle.iconData, + 'mock-icon': mockIcon, + 'mock-icon-circle': mockIcon2, + 'mock-icon-2': mockIcon2, + 'legacy-icon': legacyIconData, + 'legacy-old-name': legacyIconData, + 'other-legacy-icon': otherLegacyIconData, + OtherLegacyIcon: otherLegacyIconData, + ['circle']: LucideCircle.icon, }); }); }); diff --git a/packages/angular/src/lucide-icons.ts b/packages/angular/src/lucide-icons.ts index 9fa0f8fe6..0f3136b35 100644 --- a/packages/angular/src/lucide-icons.ts +++ b/packages/angular/src/lucide-icons.ts @@ -1,7 +1,5 @@ import { InjectionToken, Provider } from '@angular/core'; -import { LucideIconData, LucideIcons } from './types'; -import { isLucideIconComponent, LucideIconComponentType } from './types'; -import { toKebabCase } from './utils/to-kebab-case'; +import { isLucideIconComponent, LucideIcon, LucideIconData, LucideIcons } from './types'; /** * Injection token for providing Lucide icons by name. @@ -23,7 +21,7 @@ export const LUCIDE_ICONS = new InjectionToken('Lucide icons', { * @usage * ```ts * import { provideLucideIcons, SquareCheck } from '@lucide/angular'; - * import { MyCustomIcon } from './custom-icons/circle-check'; + * import { MyCustomIcon } from './custom-icons/my-custom-icon'; * * providers: [ * provideLucideIcons({ @@ -37,28 +35,71 @@ export const LUCIDE_ICONS = new InjectionToken('Lucide icons', { * * ``` */ -export function provideLucideIcons( - icons: Record | Array, -): Provider { - if (Array.isArray(icons)) { - return { - provide: LUCIDE_ICONS, - useValue: icons.reduce((acc, icon) => { - acc[toKebabCase(icon.iconName)] = icon.iconData; - return acc; - }, {} as LucideIcons), - }; - } else { - return { - provide: LUCIDE_ICONS, - useValue: Object.entries(icons).reduce((acc, [name, icon]) => { - if (isLucideIconComponent(icon)) { - acc[icon.iconName] = icon.iconData; - } else { - acc[toKebabCase(name)] = icon; - } - return acc; - }, {} as LucideIcons), - }; - } +export function provideLucideIcons(...icons: Array): Provider { + return { + provide: LUCIDE_ICONS, + useValue: icons.reduce((acc, icon) => { + const iconData = isLucideIconComponent(icon) ? icon.icon : icon; + acc[iconData.name] = iconData; + for (const alias of iconData.aliases ?? []) { + acc[alias] = iconData; + } + return acc; + }, {} as LucideIcons), + }; +} + +/** + * Converts a legacy icon node to the new format, for custom icon (e.g. `@lucide/lab`) support. + * + * @usage + * ```ts + * import { provideLucideIcons, lucideLegacyIcon } from '@lucide/angular'; + * import { UserRoundX } from 'lucide-angular'; + * import { burger } from '@lucide/lab'; + * + * provideLucideIcons( + * lucideLegacyIcon('user-round-x', UserRoundX, ['user-circle-x']), + * lucideLegacyIcon('burger', burger, ['hamburger']), + * ), + * ``` + */ +export function lucideLegacyIcon( + name: string, + node: LucideIconData['node'], + aliases: string[] = [], +): LucideIconData { + return { + name, + node, + aliases, + }; +} + +/** + * Converts a map of legacy icon nodes to a list of icon data objects. + * + * @usage + * ```ts + * import { provideLucideIcons, lucideLegacyIconMap, LucideCircle } from '@lucide/angular'; + * import { UserRoundX } from 'lucide-angular'; + * import { burger } from '@lucide/lab'; + * + * provideLucideIcons( + * LucideCircle, + * ...lucideLegacyIconMap({ UserRoundX, burger }), + * ), + * ``` + */ +export function lucideLegacyIconMap( + icons: Record, +): LucideIconData[] { + return Object.entries(icons).map(([pascalName, node]) => { + const name: string = pascalName.replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); + return { + name, + node, + aliases: [pascalName], + }; + }); } diff --git a/packages/angular/src/public-api.ts b/packages/angular/src/public-api.ts index 51d13d982..3bee2b36d 100644 --- a/packages/angular/src/public-api.ts +++ b/packages/angular/src/public-api.ts @@ -1,7 +1,7 @@ import * as icons from './icons/lucide-angular'; export * from './lucide-config'; -export * from './lucide-icon'; +export * from './lucide-dynamic-icon'; export * from './lucide-icons'; export * from './types'; export * from './icons/lucide-angular'; diff --git a/packages/angular/src/types.ts b/packages/angular/src/types.ts index a3f7b23b4..9e210dca1 100644 --- a/packages/angular/src/types.ts +++ b/packages/angular/src/types.ts @@ -2,44 +2,57 @@ import { Signal, Type } from '@angular/core'; type HtmlAttributes = { [key: string]: string | number }; export type LucideIconNode = readonly [string, HtmlAttributes]; -export type LucideIconData = readonly LucideIconNode[]; export type LucideIcons = { [key: string]: LucideIconData }; /** - * Represents a Lucide icon component that has `iconName` and `iconData` signals inherited from `LucideIconBase` and respective static members accessible without instantiating the component. + * A Lucide icon object that fully describes an icon to be displayed. */ -export type LucideIconComponentType = Type<{ +export type LucideIconData = { + name: string; + node: LucideIconNode[]; + aliases?: string[]; +}; + +/** + * Input signal map of Lucide icon components. + */ +interface LucideIconProps { title: Signal>; size: Signal>; color: Signal>; strokeWidth: Signal>; absoluteStrokeWidth: Signal>; -}> & { - iconName: string; - iconData: LucideIconData; -}; +} + +/** + * Represents a Lucide icon component type that has `iconName` and `iconData` signals inherited from `LucideIconBase` and respective static members accessible without instantiating the component. + */ +export interface LucideIcon extends Type { + icon: LucideIconData; +} /** * Type guard for {@link LucideIconData} */ export function isLucideIconData(icon: unknown): icon is LucideIconData { - return Array.isArray(icon); -} - -/** - * Type guard for {@link LucideIconComponentType} - */ -export function isLucideIconComponent(icon: unknown): icon is LucideIconComponentType { return ( - icon instanceof Type && - 'iconData' in icon && - Array.isArray(icon.iconData) && - 'iconName' in icon && - typeof icon.iconName === 'string' + !!icon && + typeof icon === 'object' && + 'name' in icon && + typeof icon.name === 'string' && + 'node' in icon && + Array.isArray(icon.node) ); } -export type LucideIconInput = LucideIconComponentType | LucideIconData | string; +/** + * Type guard for {@link LucideIcon} + */ +export function isLucideIconComponent(icon: unknown): icon is LucideIcon { + return icon instanceof Type && 'icon' in icon && isLucideIconData(icon.icon); +} + +export type LucideIconInput = LucideIcon | LucideIconData | string; /** * @internal diff --git a/packages/angular/src/utils/format-fixed.ts b/packages/angular/src/utils/format-fixed.ts deleted file mode 100644 index 0a35fc24b..000000000 --- a/packages/angular/src/utils/format-fixed.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function formatFixed(number: number, decimals = 3): string { - return parseFloat(number.toFixed(decimals)).toString(10); -} diff --git a/packages/angular/src/utils/to-kebab-case.ts b/packages/angular/src/utils/to-kebab-case.ts deleted file mode 100644 index 8752ee821..000000000 --- a/packages/angular/src/utils/to-kebab-case.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const toKebabCase = (name: string) => - name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();