feat(packages/angular): switched to self-describing IconData object from separate node+name – no more toKebabCase hackery

feat(packages/angular): renamed LucideIconComponentType => LucideIcon, and LucideIcon => LucideDynamicIcon
feat(packages/angular): added backwards compatible CSS class support
feat(packages/angular): switched to vector-effect: non-scaling-stroke implementation from computed stroke width
feat(packages/angular): rewrote icon provider to only accept a list of self-described icons – no more toKebabCase hackery & as an added bonus automatic backwards compatible alias support 🚀
feat(packages/angular): added legacy icon node helper function for passing legacy icons to providers
test(packages/angular): added unit tests on LUCIDE_CONFIG provider usage
This commit is contained in:
Karsa
2026-02-23 13:20:12 +01:00
parent d225ad8ab8
commit 032abc5e31
12 changed files with 327 additions and 229 deletions

View File

@@ -8,3 +8,4 @@ node_modules
docs/images
docs/**/examples/
packages/lucide-react/dynamicIconImports.js
packages/angular/.angular

View File

@@ -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) => {

View File

@@ -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';
<rect x="1" y="1" width="22" height="22" />
</svg>
}`,
imports: [LucideIcon],
imports: [LucideDynamicIcon],
})
class TestHostComponent {
readonly icon = input<LucideIconData>();
}
describe('LucideIcon', () => {
let component: LucideIcon;
let fixture: ComponentFixture<LucideIcon>;
describe('LucideDynamicIcon', () => {
let component: LucideDynamicIcon;
let fixture: ComponentFixture<LucideDynamicIcon>;
let icon: WritableSignal<LucideIconInput | null | undefined>;
let name: WritableSignal<string | undefined>;
let title: WritableSignal<string | undefined>;
let color: WritableSignal<string | undefined>;
let size: WritableSignal<string | number | undefined>;
let strokeWidth: WritableSignal<string | number | undefined>;
let absoluteStrokeWidth: WritableSignal<boolean | undefined>;
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(
'<!--container--><polyline points="1 1 22 22"></polyline>',
);
});
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('<rect x="1" y="1" width="22" height="22"></rect>');
});
});
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();
}
});
});
});
});

View File

@@ -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<LucideIconInput | null>();
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;
});
}

View File

@@ -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<string | number>,
@@ -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<Nullable<string>>;
protected abstract readonly iconData: Signal<Nullable<LucideIconData>>;
protected abstract readonly icon: Signal<Nullable<LucideIconData>>;
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<boolean>) => 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);
}
});
}
});

View File

@@ -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<string | null>();
readonly iconInput = input.required<LucideIconInput | null>({
alias: 'lucideIcon',
});
readonly resolvedIcon = computed<LucideResolvedIcon | null>(() => {
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;
}
}

View File

@@ -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,
});
});
});

View File

@@ -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<LucideIcons>('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<LucideIcons>('Lucide icons', {
* <svg lucideIcon="my-custom-icon" />
* ```
*/
export function provideLucideIcons(
icons: Record<string, LucideIconData | LucideIconComponentType> | Array<LucideIconComponentType>,
): 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<LucideIcon | LucideIconData>): 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<string, LucideIconData['node']>,
): 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],
};
});
}

View File

@@ -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';

View File

@@ -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<Nullable<string>>;
size: Signal<Nullable<number>>;
color: Signal<Nullable<string>>;
strokeWidth: Signal<Nullable<number>>;
absoluteStrokeWidth: Signal<Nullable<boolean>>;
}> & {
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<LucideIconProps> {
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

View File

@@ -1,3 +0,0 @@
export function formatFixed(number: number, decimals = 3): string {
return parseFloat(number.toFixed(decimals)).toString(10);
}

View File

@@ -1,2 +0,0 @@
export const toKebabCase = (name: string) =>
name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();