mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-14 13:58:21 +00:00
feat(): unify icon build logic some more, add nonScalingStroke option, marking absoluteStrokeWidth as deprecated
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
"prebuild": "pnpm clean && pnpm copy:license && pnpm copy:utils && pnpm build:icons",
|
||||
"build": "pnpm prebuild && pnpm build:ng",
|
||||
"copy:license": "cp ../../LICENSE ./LICENSE",
|
||||
"copy:utils": "mkdir -p ./src/utils && cp -f ../../packages/shared/src/build/buildLucideIconNode.ts ./src/utils/ && cp -f ../../packages/shared/src/build/types.ts ./src/utils/",
|
||||
"copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/mergeClasses.ts ./src/utils/",
|
||||
"clean": "rm -rf dist && rm -rf ./src/icons/*.ts",
|
||||
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=lucide-angular.ts --useDefaultExports=0",
|
||||
"build:ng": "ng build --configuration production",
|
||||
|
||||
@@ -19,6 +19,11 @@ export interface LucideConfig {
|
||||
* @default 2
|
||||
*/
|
||||
strokeWidth: number;
|
||||
/**
|
||||
* Whether to use absolute stroke width.
|
||||
* @deprecated Use `nonScalingStroke` instead.
|
||||
*/
|
||||
absoluteStrokeWidth: boolean;
|
||||
/**
|
||||
* Whether stroke width should be scaled to appear uniform regardless of icon size.
|
||||
* @default false
|
||||
@@ -31,7 +36,7 @@ export interface LucideConfig {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
absoluteStrokeWidth: boolean;
|
||||
nonScalingStroke: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,6 +47,7 @@ export const lucideDefaultConfig: LucideConfig = {
|
||||
size: 24,
|
||||
strokeWidth: 2,
|
||||
absoluteStrokeWidth: false,
|
||||
nonScalingStroke: false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,6 +41,7 @@ describe('LucideIconBase', () => {
|
||||
let size: WritableSignal<string | number | undefined>;
|
||||
let strokeWidth: WritableSignal<string | number | undefined>;
|
||||
let absoluteStrokeWidth: WritableSignal<boolean | undefined>;
|
||||
let nonScalingStroke: WritableSignal<boolean | undefined>;
|
||||
const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr);
|
||||
function createComponent() {
|
||||
return TestBed.createComponent(LucideCircleCheck, {
|
||||
@@ -51,6 +52,7 @@ describe('LucideIconBase', () => {
|
||||
inputBinding('size', size),
|
||||
inputBinding('strokeWidth', strokeWidth),
|
||||
inputBinding('absoluteStrokeWidth', absoluteStrokeWidth),
|
||||
inputBinding('nonScalingStroke', nonScalingStroke),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -60,6 +62,7 @@ describe('LucideIconBase', () => {
|
||||
size = signal(undefined);
|
||||
strokeWidth = signal(undefined);
|
||||
absoluteStrokeWidth = signal(undefined);
|
||||
nonScalingStroke = signal(undefined);
|
||||
fixture = createComponent();
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
@@ -113,6 +116,11 @@ describe('LucideIconBase', () => {
|
||||
expect(getSvgAttribute('width')).toBe('18');
|
||||
expect(getSvgAttribute('height')).toBe('18');
|
||||
});
|
||||
it('should set viewBox', () => {
|
||||
size.set('18');
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('viewBox')).toBe('0 0 24 24');
|
||||
});
|
||||
});
|
||||
|
||||
describe('strokeWidth', () => {
|
||||
@@ -133,21 +141,37 @@ describe('LucideIconBase', () => {
|
||||
});
|
||||
|
||||
describe('absoluteStrokeWidth', () => {
|
||||
it('should adjust stroke width', () => {
|
||||
strokeWidth.set(2);
|
||||
size.set(32);
|
||||
absoluteStrokeWidth.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('1.5');
|
||||
});
|
||||
it('should not set vector-effect on children', () => {
|
||||
absoluteStrokeWidth.set(true);
|
||||
for (const child of fixture.nativeElement.children) {
|
||||
expect(child.getAttribute('vector-effect')).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonScalingStroke', () => {
|
||||
it('should not adjust stroke width', () => {
|
||||
strokeWidth.set(2);
|
||||
size.set(12);
|
||||
absoluteStrokeWidth.set(true);
|
||||
nonScalingStroke.set(false);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('2');
|
||||
});
|
||||
it('should not set vector-effect on children', () => {
|
||||
absoluteStrokeWidth.set(false);
|
||||
nonScalingStroke.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);
|
||||
nonScalingStroke.set(true);
|
||||
for (const child of fixture.nativeElement.children) {
|
||||
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
|
||||
}
|
||||
@@ -205,12 +229,12 @@ describe('LucideIconBase', () => {
|
||||
describe('strokeWidth', () => {
|
||||
it('should use stroke width from config', () => {
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('1');
|
||||
expect(getSvgAttribute('stroke-width')).toBe('2');
|
||||
});
|
||||
it('should use override stroke width from config', () => {
|
||||
strokeWidth.set(3);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('3');
|
||||
expect(getSvgAttribute('stroke-width')).toBe('6');
|
||||
});
|
||||
});
|
||||
describe('size', () => {
|
||||
@@ -239,5 +263,18 @@ describe('LucideIconBase', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('nonScalingStroke', () => {
|
||||
it('should use nonScalingStroke from config', () => {
|
||||
for (const child of fixture.nativeElement.children) {
|
||||
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
|
||||
}
|
||||
});
|
||||
it('should override nonScalingStroke', () => {
|
||||
nonScalingStroke.set(false);
|
||||
for (const child of fixture.nativeElement.children) {
|
||||
expect(child.getAttribute('vector-effect')).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { LUCIDE_CONFIG } from './lucide-config';
|
||||
import { LucideIconData, Nullable } from './types';
|
||||
import { LucideIconData, LucideIconNode, Nullable } from './types';
|
||||
import defaultAttributes from './default-attributes';
|
||||
import { lucideIconTemplate } from './lucide-icon-template';
|
||||
import buildLucideIconNode from './utils/buildLucideIconNode';
|
||||
@@ -23,12 +24,13 @@ import buildLucideIconNode from './utils/buildLucideIconNode';
|
||||
template: lucideIconTemplate,
|
||||
host: {
|
||||
...defaultAttributes,
|
||||
class: 'lucide',
|
||||
'[attr.width]': 'size()',
|
||||
'[attr.height]': 'size()',
|
||||
'[attr.stroke]': 'color()',
|
||||
'[attr.stroke-width]': 'strokeWidth()',
|
||||
'[attr.aria-hidden]': '!title()',
|
||||
'[attr.class]': 'computedClass()',
|
||||
'[attr.width]': 'computedWidth()',
|
||||
'[attr.height]': 'computedHeight()',
|
||||
'[attr.viewBox]': 'computedViewbox()',
|
||||
'[attr.stroke]': 'computedStroke()',
|
||||
'[attr.stroke-width]': 'computedStrokeWidth()',
|
||||
'[attr.aria-hidden]': 'computedAriaHidden()',
|
||||
},
|
||||
})
|
||||
export abstract class LucideIconBase {
|
||||
@@ -57,6 +59,16 @@ export abstract class LucideIconBase {
|
||||
readonly size = input(this.iconConfig.size, {
|
||||
transform: (value: Nullable<string | number>) => value ?? this.iconConfig.size,
|
||||
});
|
||||
/**
|
||||
* Icon width.
|
||||
* @default 24
|
||||
*/
|
||||
readonly width = input(null);
|
||||
/**
|
||||
* Icon height.
|
||||
* @default 24
|
||||
*/
|
||||
readonly height = input(null);
|
||||
/**
|
||||
* Stroke color.
|
||||
* @default currentColor
|
||||
@@ -72,36 +84,105 @@ export abstract class LucideIconBase {
|
||||
transform: (value: Nullable<string | number>) => value ?? this.iconConfig.strokeWidth,
|
||||
});
|
||||
/**
|
||||
* 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.
|
||||
* Whether to use absolute stroke width.
|
||||
*/
|
||||
readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, {
|
||||
transform: (value: Nullable<boolean>) => value ?? this.iconConfig.absoluteStrokeWidth,
|
||||
});
|
||||
/**
|
||||
* 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 nonScalingStroke = input(this.iconConfig.nonScalingStroke, {
|
||||
transform: (value: Nullable<boolean>) => value ?? this.iconConfig.nonScalingStroke,
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedIcon = computed(() => {
|
||||
const icon = this.icon();
|
||||
return icon
|
||||
? buildLucideIconNode(icon, {
|
||||
width: this.width() ?? this.size(),
|
||||
height: this.height() ?? this.size(),
|
||||
color: this.color(),
|
||||
strokeWidth: this.strokeWidth(),
|
||||
absoluteStrokeWidth: this.absoluteStrokeWidth(),
|
||||
nonScalingStroke: this.nonScalingStroke(),
|
||||
hasA11yProp: this.title() != null,
|
||||
})
|
||||
: null;
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedAttributes = computed<LucideIconNode[1] | undefined>(
|
||||
() => this.computedIcon()?.[1],
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedIconNode = computed(() => this.computedIcon()?.[2]);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedWidth = computed(
|
||||
() => this.computedAttributes()?.['width'] ?? this.iconConfig.size,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedHeight = computed(
|
||||
() => this.computedAttributes()?.['height'] ?? this.iconConfig.size,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedStroke = computed(
|
||||
() => this.computedAttributes()?.['stroke'] ?? this.iconConfig.color,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedStrokeWidth = computed(
|
||||
() => this.computedAttributes()?.['stroke-width'] ?? this.iconConfig.strokeWidth,
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedAriaHidden = computed(
|
||||
() => this.computedAttributes()?.['aria-hidden'] === 'true',
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedViewbox = computed(
|
||||
() => this.computedAttributes()?.['viewBox'] ?? '0 0 24 24',
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly computedClass = computed(
|
||||
() => this.computedAttributes()?.['class'] ?? 'lucide',
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect((onCleanup) => {
|
||||
const icon = this.icon();
|
||||
if (icon) {
|
||||
const absoluteStrokeWidth = this.absoluteStrokeWidth();
|
||||
const [, iconAttributes, iconNode] = buildLucideIconNode(icon as any, {
|
||||
absoluteStrokeWidth,
|
||||
hasA11yProp: this.title() != null,
|
||||
});
|
||||
|
||||
const classes =
|
||||
typeof iconAttributes['class'] === 'string'
|
||||
? iconAttributes['class']
|
||||
.split(' ')
|
||||
.filter((cssClass) => cssClass && cssClass !== 'lucide')
|
||||
: [];
|
||||
|
||||
for (const cssClass of classes) {
|
||||
this.renderer.addClass(this.elRef.nativeElement, cssClass);
|
||||
}
|
||||
|
||||
const iconNode = this.computedIconNode();
|
||||
const iconAttributes = this.computedIconNode();
|
||||
if (iconNode && iconAttributes) {
|
||||
const contentRef = this.contentRef();
|
||||
const refChild = contentRef.nativeElement;
|
||||
const elements = (iconNode ?? []).map(([name, attrs]) => {
|
||||
const elements = iconNode.map(([name, attrs]) => {
|
||||
const element = this.renderer.createElement(name, 'http://www.w3.org/2000/svg');
|
||||
|
||||
Object.entries(attrs).forEach(([name, value]) =>
|
||||
@@ -118,9 +199,6 @@ export abstract class LucideIconBase {
|
||||
elements.forEach((element) =>
|
||||
this.renderer.removeChild(this.elRef.nativeElement, element),
|
||||
);
|
||||
for (const cssClass of classes) {
|
||||
this.renderer.removeClass(this.elRef.nativeElement, cssClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { Signal, Type } from '@angular/core';
|
||||
import {
|
||||
LucideIconNode as SharedLucideIconNode,
|
||||
LucideIconData as SharedLucideIconData,
|
||||
} from './utils/types';
|
||||
|
||||
type HtmlAttributes = { [key: string]: string | number };
|
||||
export type LucideIconNode = readonly [string, HtmlAttributes];
|
||||
export type LucideIconNode = SharedLucideIconNode;
|
||||
export type LucideIcons = { [key: string]: LucideIconData };
|
||||
|
||||
/**
|
||||
* A Lucide icon object that fully describes an icon to be displayed.
|
||||
*/
|
||||
export type LucideIconData = {
|
||||
name: string;
|
||||
size?: number;
|
||||
node: LucideIconNode[];
|
||||
aliases?: string[];
|
||||
};
|
||||
export type LucideIconData = SharedLucideIconData & { name: string };
|
||||
|
||||
/**
|
||||
* Input signal map of Lucide icon components.
|
||||
@@ -22,7 +20,11 @@ interface LucideIconProps {
|
||||
size: Signal<Nullable<number | string>>;
|
||||
color: Signal<Nullable<string>>;
|
||||
strokeWidth: Signal<Nullable<number | string>>;
|
||||
/**
|
||||
* @deprecated Use `nonScalingStroke` instead.
|
||||
*/
|
||||
absoluteStrokeWidth: Signal<Nullable<boolean>>;
|
||||
nonScalingStroke: Signal<Nullable<boolean>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user