Merge branch 'package/extract-build-logic' into plan-lucide-react-integration-tests

This commit is contained in:
Eric Fennis
2026-09-04 13:41:38 +02:00
125 changed files with 3058 additions and 1484 deletions
+3
View File
@@ -28,6 +28,9 @@ packages/**/src/aliases.ts
!packages/**/src/aliases/index.ts
packages/**/src/utils/*.ts
!packages/shared/src/utils/*.ts
!packages/shared/src/build/
!packages/shared/src/build/*.ts
packages/shared/src/build/types.d.ts
packages/**/src/dynamicIconImports.ts
packages/**/DynamicIcon.d.ts
packages/**/dynamicIconImports.js
+1 -1
View File
@@ -60,7 +60,7 @@
"@vitest/coverage-v8": "4.1.10",
"@vitest/ui": "4.1.10",
"ajv-cli": "^5.0.0",
"cspell": "^10.0.1",
"cpy-cli": "^7.0.0",
"dotenv": "^17.4.2",
"eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
+5 -4
View File
@@ -17,15 +17,16 @@
"scripts": {
"ng": "ng",
"watch": "ng build --watch --configuration development",
"prebuild": "pnpm clean && pnpm copy:license && pnpm build:icons",
"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": "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",
"test": "pnpm build:icons && ng test --no-watch",
"test:watch": "ng test",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
"test": "pnpm copy:utils && pnpm build:icons && ng test --no-watch",
"test:watch": "pnpm copy:utils && ng test",
"lint": "npx eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
"e2e": "ng e2e"
},
"prettier": {
+7 -1
View File
@@ -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,
};
/**
@@ -8,7 +8,7 @@ import { LucideBadgeAlert } from './icons/badge-alert';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'lucide-static-hydration-root',
template: `<svg lucideBadgeAlert [absoluteStrokeWidth]="true"></svg>`,
template: `<svg lucideBadgeAlert [nonScalingStroke]="true"></svg>`,
imports: [LucideBadgeAlert],
})
class StaticHydrationRootComponent {}
+43 -9
View File
@@ -47,6 +47,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);
const expectSvgClasses = (classes: string[], svg: SVGElement = fixture.nativeElement) => {
for (const cssClass of classes) {
@@ -63,6 +64,7 @@ describe('LucideIconBase', () => {
inputBinding('size', size),
inputBinding('strokeWidth', strokeWidth),
inputBinding('absoluteStrokeWidth', absoluteStrokeWidth),
inputBinding('nonScalingStroke', nonScalingStroke),
],
});
}
@@ -72,6 +74,7 @@ describe('LucideIconBase', () => {
size = signal(undefined);
strokeWidth = signal(undefined);
absoluteStrokeWidth = signal(undefined);
nonScalingStroke = signal(undefined);
fixture = createComponent();
component = fixture.componentInstance;
});
@@ -135,6 +138,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', () => {
@@ -155,22 +163,38 @@ 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);
fixture.detectChanges();
for (const child of getRenderedChildren()) {
expect(child.getAttribute('vector-effect')).toBeNull();
}
});
it('should set vector-effect on children', () => {
absoluteStrokeWidth.set(true);
nonScalingStroke.set(true);
fixture.detectChanges();
for (const child of getRenderedChildren()) {
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
@@ -207,6 +231,7 @@ describe('LucideIconBase', () => {
color: 'red',
strokeWidth: 1,
size: 12,
nonScalingStroke: true,
absoluteStrokeWidth: true,
}),
],
@@ -229,12 +254,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', () => {
@@ -253,14 +278,23 @@ describe('LucideIconBase', () => {
describe('absoluteStrokeWidth', () => {
it('should use absoluteStrokeWidth from config', () => {
fixture.detectChanges();
for (const child of getRenderedChildren()) {
expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke');
}
expect(getSvgAttribute('stroke-width')).toBe('2');
});
it('should override absoluteStrokeWidth', () => {
absoluteStrokeWidth.set(false);
fixture.detectChanges();
for (const child of getRenderedChildren()) {
expect(getSvgAttribute('stroke-width')).toBe('1');
});
});
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();
}
});
+110 -30
View File
@@ -1,8 +1,9 @@
import { Component, computed, inject, input, Signal } 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';
/**
* @internal
@@ -12,38 +13,17 @@ import { lucideIconTemplate } from './lucide-icon-template';
template: lucideIconTemplate,
host: {
...defaultAttributes,
class: 'lucide',
'[class]': 'iconClasses()',
'[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 {
protected abstract readonly icon: Signal<Nullable<LucideIconData>>;
protected readonly iconNodes = computed<LucideIconData['node']>(() => {
const node = this.icon()?.node ?? [];
if (!this.absoluteStrokeWidth()) {
return node;
}
return node.map<LucideIconData['node'][number]>(([name, attrs]) => [
name,
{
'vector-effect': 'non-scaling-stroke',
...attrs,
},
]);
});
protected readonly iconClasses = computed(() => {
const icon = this.icon();
if (!icon) {
return '';
}
const { name, aliases = [] } = icon;
return [name, ...aliases].map((item) => `lucide-${item}`).join(' ');
});
protected readonly iconConfig = inject(LUCIDE_CONFIG);
/**
* An optional accessible label for the icon.
@@ -58,6 +38,10 @@ export abstract class LucideIconBase {
* - If your icon is functional (e.g. used in place of a label), feel free to use this property.
*/
readonly title = input<Nullable<string>>();
/**
* Optional CSS classes for the icon.
*/
readonly class = input<Nullable<string>>();
/**
* Width and height.
* @default 24
@@ -65,6 +49,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
@@ -80,9 +74,95 @@ 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(),
className: this.class() ?? undefined,
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',
);
}
+1 -1
View File
@@ -6,7 +6,7 @@ export const lucideIconTemplate = `@if (title(); as titleValue) {
<title>{{ titleValue }}</title>
}
<ng-content select="title"></ng-content>
@for (child of iconNodes(); track child[1]['key'] ?? $index) {
@for (child of computedIconNode(); track child[1]['key'] ?? $index) {
@let attrs = child[1];
@switch (child[0]) {
@case ('path') {
+10 -8
View File
@@ -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>>;
}
/**
+1 -1
View File
@@ -43,7 +43,7 @@
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts --pretty=false",
"clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts",
"copy:license": "cp ../../LICENSE ./LICENSE",
"copy:utils": "mkdir -p ./src/utils && for f in hasA11yProp toKebabCase mergeClasses; do cp -f ../../packages/shared/src/utils/$f.ts ./src/utils/; done",
"copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/{hasA11yProp.ts,toKebabCase.ts,mergeClasses.ts} ./src/utils/",
"test": "pnpm copy:utils && pnpm build:icons && vitest run",
"test:watch": "pnpm build:icons && vitest run --watch"
},
+21 -21
View File
@@ -1,28 +1,27 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(async ({
componentName,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
export default defineExportTemplate(
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
// Astro doesn't need keyed children in loops
const keylessChildren = children.map((c) => {
const [element, { key, ...otherAttrs }] = c;
return [element, otherAttrs];
});
// Astro doesn't need keyed children in loops
const keylessIconData = {
...iconData,
node: iconData.node.map((child) => {
const [element, { key, ...otherAttrs }] = child;
return [element, otherAttrs];
}),
};
// TODO: build-icons' `pretty` is set to false as the prettier
// formatter uses babel which I'm not sure it supports typescript
return `
// TODO: build-icons' `pretty` is set to false as the prettier
// formatter uses babel which I'm not sure it supports typescript
return `
import createLucideIcon from '../createLucideIcon';
import type { AstroComponent } from '../types'
import type { AstroComponent, LucideIconData } from '../types'
const iconData: LucideIconData = ${JSON.stringify(keylessIconData)};
/**
* @component @name ${componentName}
@@ -35,8 +34,9 @@ import type { AstroComponent } from '../types'
* @returns {any} Astro Component
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName} = createLucideIcon('${iconName}', ${JSON.stringify(keylessChildren)}) as AstroComponent;
const ${componentName} = createLucideIcon(iconData) as AstroComponent;
export default ${componentName};
`;
});
},
);
+28 -16
View File
@@ -1,32 +1,44 @@
---
import defaultAttributes from './defaultAttributes';
import buildLucideIconNode from './utils/buildLucideIconNode';
import type { IconProps as Props } from './types';
import { hasA11yProp } from './utils/hasA11yProp';
const {
color = 'currentColor',
size = 24,
width,
height,
'stroke-width': strokeWidth = 2,
absoluteStrokeWidth = false,
iconNode = [],
nonScalingStroke = false,
iconNode,
icon = {
node: iconNode,
aliases: [],
size: 24,
},
class: className,
...rest
} = Astro.props;
const [, builtIconAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
color,
width: width ?? size,
height: height ?? size,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
className,
hasA11yProp: hasA11yProp(rest),
});
const iconAttributes = {
...builtIconAttributes,
...rest,
};
---
<svg
{...{
...defaultAttributes,
width: size,
height: size,
stroke: color,
'stroke-width': absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
...(!hasA11yProp(rest) && { 'aria-hidden': 'true' }),
...rest,
}}
class:list={['lucide', className]}
>
{iconNode.map(([Tag, attrs]) => <Tag {...attrs} />)}
<svg {...iconAttributes}>
{builtIconNode.map(([Tag, attrs]) => <Tag {...attrs} />)}
<slot />
</svg>
+41 -10
View File
@@ -1,11 +1,43 @@
import type { AstroComponentFactory } from 'astro/runtime/server/render/astro/factory.js';
import type { IconNode } from './types';
import type { LucideIconData, LucideIconNode } from './types';
import { render, renderSlot, createComponent, renderComponent } from 'astro/compiler-runtime';
import Icon from './Icon.astro';
import { mergeClasses } from './utils/mergeClasses';
import { toKebabCase } from './utils/toKebabCase';
export default (iconName: string, iconNode: IconNode): AstroComponentFactory => {
/**
* Creates a Lucide icon component from icon data.
*
* @param icon Icon data object.
* @returns Lucide icon component.
*/
function createLucideIcon(icon: LucideIconData): AstroComponentFactory;
/**
* Creates a Lucide icon component from the legacy icon arguments.
*
* @param iconName Icon name.
* @param iconNode Icon node.
* @param aliases Optional icon aliases.
* @returns Lucide icon component.
*/
function createLucideIcon(
iconName: string,
iconNode: LucideIconNode[],
aliases?: string[],
): AstroComponentFactory;
function createLucideIcon(
iconOrIconName: string | LucideIconData,
iconNode: LucideIconNode[] = [],
aliases: string[] = [],
): AstroComponentFactory {
const icon =
typeof iconOrIconName === 'string'
? {
name: iconOrIconName,
node: iconNode,
aliases,
}
: iconOrIconName;
const Component = createComponent(
($$result, $$props: Record<string, unknown>, $$slots) => {
const { class: className, ...restProps } = $$props;
@@ -14,11 +46,8 @@ export default (iconName: string, iconNode: IconNode): AstroComponentFactory =>
'Icon',
Icon,
{
class: mergeClasses(
Boolean(iconName) && `lucide-${toKebabCase(iconName)}`,
Boolean(className) && className,
),
iconNode,
class: className,
icon,
...restProps,
},
{ default: () => render`${renderSlot($$result, $$slots['default'])}` },
@@ -28,4 +57,6 @@ export default (iconName: string, iconNode: IconNode): AstroComponentFactory =>
'none',
);
return Component;
};
}
export default createLucideIcon;
-15
View File
@@ -1,15 +0,0 @@
import type { SVGAttributes } from './types';
const defaultAttributes: SVGAttributes = {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
};
export default defaultAttributes;
+1 -1
View File
@@ -3,6 +3,6 @@ export * as icons from './icons/index';
export * from './aliases';
export * from './types';
export { default as defaultAttributes } from './defaultAttributes';
export { default as defaultAttributes } from './utils/defaultAttributes';
export { default as createLucideIcon } from './createLucideIcon';
export { default as Icon } from './Icon.astro';
+30 -6
View File
@@ -1,23 +1,47 @@
import type { HTMLAttributes } from 'astro/types';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from './utils/types';
// Type that the Astro language server needs to infer component props in Astro files
export type AstroComponent = (_props: IconProps) => unknown;
export type AstroComponent = (_props: LucideProps) => any;
export interface IconProps extends SVGAttributes {
export type LucideProps = SVGAttributes & {
color?: string;
size?: number | string;
'stroke-width'?: number | string;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
class?: string;
iconNode?: IconNode;
title?: string;
}
};
export type IconProps = LucideProps &
(
| {
icon: LucideIconData;
iconNode?: never;
}
| {
icon?: never;
iconNode: LucideIconNode[];
}
);
export type SVGAttributes = HTMLAttributes<'svg'>;
export type IconNode = IconNodeChild[];
export type LucideIconNode = SharedLucideIconNode<SVGElements, SVGAttributes>;
type IconNodeChild = [elementName: SVGElements, attrs: SVGAttributes];
export type LucideIconData = SharedLucideIconData<SVGElements, SVGAttributes>;
/**
* @deprecated Use LucideIconNode instead.
*/
export type IconNode = LucideIconNode[];
// All possible svg elements according to the Astro definitions
type SVGElements =
@@ -10,8 +10,8 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = `
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide"
aria-hidden="true"
>
<path d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2">
</path>
@@ -10,8 +10,8 @@ exports[`Using createLucideIcon > should create a component from an iconNode 1`]
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-air-vent"
aria-hidden="true"
>
<path d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2">
</path>
@@ -10,9 +10,9 @@ exports[`Using lucide icon components > should add a non-default attribute to th
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-smile"
aria-hidden="true"
style="position: absolute"
class="lucide lucide-face-slightly-smiling"
>
<path d="M15 10V9">
</path>
@@ -38,8 +38,8 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
height="18"
@@ -69,41 +69,50 @@ exports[`Using lucide icon components > should apply all classes to the element
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-droplet lucide-drop my-icon"
aria-hidden="true"
class="lucide lucide-droplet my-icon"
>
<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z">
</path>
</svg>
`;
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
width="12"
height="12"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
<rect vector-effect="non-scaling-stroke"
width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path d="M3 9h18">
<path vector-effect="non-scaling-stroke"
d="M3 9h18"
>
</path>
<path d="M3 15h18">
<path vector-effect="non-scaling-stroke"
d="M3 15h18"
>
</path>
<path d="M9 3v18">
<path vector-effect="non-scaling-stroke"
d="M9 3v18"
>
</path>
<path d="M15 3v18">
<path vector-effect="non-scaling-stroke"
d="M15 3v18"
>
</path>
</svg>
`;
@@ -118,8 +127,8 @@ exports[`Using lucide icon components > should pass children to the icon slot 1`
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-smile"
aria-hidden="true"
class="lucide lucide-face-slightly-smiling"
>
<path d="M15 10V9">
</path>
@@ -148,8 +157,8 @@ exports[`Using lucide icon components > should render a component 1`] = `
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
height="18"
@@ -179,8 +188,39 @@ exports[`Using lucide icon components > should render the icon with default attr
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path d="M3 9h18">
</path>
<path d="M3 15h18">
</path>
<path d="M9 3v18">
</path>
<path d="M15 3v18">
</path>
</svg>
`;
exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
height="18"
@@ -5,7 +5,10 @@ import { render } from './utils';
describe('Using createLucideIcon', () => {
it('should create a component from an iconNode', async () => {
const AirVent = createLucideIcon('AirVent', airVent);
const AirVent = createLucideIcon({
name: 'air-vent',
node: airVent,
});
const { container } = await render(AirVent);
expect(container.innerHTML).toBeDefined();
+38 -5
View File
@@ -1,7 +1,8 @@
import { describe, it, expect } from 'vitest';
import { Pen, Edit2, Grid, Droplet, Smile, Rocket } from '../src/lucide-astro';
import defaultAttributes from '../src/defaultAttributes';
import { Pen, Edit2, Grid, Droplet, Smile, Rocket, type LucideIconData } from '../src/lucide-astro';
import defaultAttributes from '../src/utils/defaultAttributes';
import { createAstroHTMLString, render } from './utils';
import createLucideIcon from '../src/createLucideIcon.ts';
describe('Using lucide icon components', () => {
it('should render a component', async () => {
@@ -64,11 +65,11 @@ describe('Using lucide icon components', () => {
expect(PenIconContainer.innerHTML).toBe(Edit2Container.innerHTML);
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', async () => {
it('should scale the strokeWidth when absoluteStrokeWidth is set', async () => {
const { container } = await render(Grid, {
props: {
size: 48,
stroke: 'red',
color: 'red',
absoluteStrokeWidth: true,
},
});
@@ -83,6 +84,24 @@ describe('Using lucide icon components', () => {
expect(container.innerHTML).toMatchSnapshot();
});
it('should apply vector-effect when nonScalingStroke is set', async () => {
const { container } = await render(Grid, {
props: {
size: 12,
nonScalingStroke: true,
},
});
const SVGElement = container.firstElementChild;
expect(SVGElement).toHaveAttribute('width', '12');
expect(SVGElement).toHaveAttribute('height', '12');
expect(SVGElement).toHaveAttribute('stroke-width', '2');
expect(SVGElement?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
expect(container.innerHTML).toMatchSnapshot();
});
it('should add a non-default attribute to the element', async () => {
const { container } = await render(Smile, {
props: {
@@ -112,7 +131,20 @@ describe('Using lucide icon components', () => {
});
it('should apply all classes to the element', async () => {
const { container } = await render(Droplet, {
const iconData: LucideIconData = {
name: 'droplet',
size: 24,
node: [
[
'path',
{
d: 'M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z',
},
],
],
aliases: ['drop'],
};
const { container } = await render(createLucideIcon(iconData), {
props: {
class: 'my-icon',
},
@@ -122,6 +154,7 @@ describe('Using lucide icon components', () => {
expect(SVGElement).toHaveClass('my-icon');
expect(SVGElement).toHaveClass('lucide');
expect(SVGElement).toHaveClass('lucide-droplet');
expect(SVGElement).toHaveClass('lucide-drop');
expect(container.innerHTML).toMatchSnapshot();
});
+3 -2
View File
@@ -63,8 +63,9 @@
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "^1.0.0",
"@lucide/rollup-plugins": "workspace:*",
"@rollup/plugin-replace": "^6.0.3",
"@testing-library/jest-dom": "^6.9.1",
"@lucide/shared": "workspace:*",
"@rollup/plugin-replace": "^6.0.2",
"@testing-library/jest-dom": "^6.6.3",
"jest-serializer-html": "^7.1.0",
"rollup": "^4.62.2",
"rollup-plugin-dts": "^6.4.1",
+6 -1
View File
@@ -2,6 +2,11 @@ import plugins from '@lucide/rollup-plugins';
import pkg from './package.json' with { type: 'json' };
import dts from 'rollup-plugin-dts';
const dtsOptions = {
includeExternal: ['@lucide/shared/types'],
tsconfig: './tsconfig.json',
};
const packageName = '@lucide/icons';
const outputFileName = 'lucide-icons';
const inputs = [`src/lucide-icons.ts`];
@@ -81,7 +86,7 @@ export default [
format: 'cjs',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
})),
...configs,
];
+1 -30
View File
@@ -1,30 +1 @@
import { LucideBuildParams, LucideIconData } from './types';
import buildLucideSvg from './buildLucideSvg';
/**
* Creates a base64 encoded data URI from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideDataUri(icon: LucideIconData, params: LucideBuildParams = {}) {
const svg = buildLucideSvg(icon, params);
// Browser
if (typeof btoa === 'function') {
// Ensure proper UTF-8 handling before base64
const utf8Bytes = new TextEncoder().encode(svg);
let binary = '';
for (const element of utf8Bytes) binary += String.fromCodePoint(element);
return `data:image/svg+xml;base64,${btoa(binary)}`;
}
// Node.js (and other JS runtimes with Buffer)
if (typeof Buffer !== 'undefined') {
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`;
}
throw new Error('No base64 encoder available in this environment.');
}
export default buildLucideDataUri;
export { buildLucideDataUri as default } from '@lucide/shared';
+1 -34
View File
@@ -1,34 +1 @@
import { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
import buildLucideIconNode from './buildLucideIconNode';
const buildDomElement = (
document: Document,
[tagName, attributes, children = []]: LucideIconNode,
): Element => {
const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
for (const [attrName, value] of Object.entries(attributes)) {
element.setAttribute(attrName, value);
}
for (const node of children) {
const childNode = buildDomElement(document, node);
element.appendChild(childNode);
}
return element;
};
/**
* Creates an SvgElement from a Lucide icon object.
*
* @param document The document to create the Element in.
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideIconElement(
document: Document,
icon: LucideIconData,
params: LucideBuildParams = {},
) {
return buildDomElement(document, buildLucideIconNode(icon, params));
}
export default buildLucideIconElement;
export { buildLucideIconElement as default } from '@lucide/shared';
+1 -55
View File
@@ -1,55 +1 @@
import { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
const defaultAttributes = {
xmlns: 'http://www.w3.org/2000/svg',
width: '24',
height: '24',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
};
/**
* Creates a Lucide icon node (an svgson-like format) from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {}): LucideIconNode {
const viewBoxWidth = ('size' in icon ? icon.size : icon.width) ?? defaultAttributes.width;
const viewBoxHeight = ('size' in icon ? icon.size : icon.height) ?? defaultAttributes.height;
const aliasClassNames = icon.aliases?.map((alias) => `lucide-${alias}`) ?? [];
const classList = ['lucide', `lucide-${icon.name}`, ...aliasClassNames];
if (params['className']) {
classList.push(params['className']);
}
const attributes = {
...defaultAttributes,
...('color' in params && { stroke: params['color'] }),
...('size' in params &&
params['size'] && {
width: params['size'].toString(10),
height: params['size'].toString(10),
}),
...('width' in params && params['width'] && { width: params['width'].toString(10) }),
...('height' in params && params['height'] && { height: params['height'].toString(10) }),
...('strokeWidth' in params &&
params['strokeWidth'] && { 'stroke-width': params['strokeWidth'].toString(10) }),
class: classList.join(' '),
viewBox: `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
...('attributes' in params && params.attributes),
};
return [
'svg',
attributes,
icon.node.map(([name, attrs]) => [
name,
params['absoluteStrokeWidth'] ? { 'vector-effect': 'non-scaling-stroke', ...attrs } : attrs,
]),
];
}
export default buildLucideIconNode;
export { buildLucideIconNode as default } from '@lucide/shared';
+1 -19
View File
@@ -1,19 +1 @@
import { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
import buildLucideIconNode from './buildLucideIconNode';
const buildDomNode = ([tagName, attributes, children = []]: LucideIconNode): string =>
`<${tagName} ${Object.entries(attributes)
.map(([attrName, value]) => `${attrName}="${value}"`)
.join(' ')}>${children?.map((child) => buildDomNode(child)).join('')}</${tagName}>`;
/**
* Creates an SVG string from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideSvg(icon: LucideIconData, params: LucideBuildParams = {}) {
return buildDomNode(buildLucideIconNode(icon, params));
}
export default buildLucideSvg;
export { buildLucideSvg as default } from '@lucide/shared';
+10 -43
View File
@@ -1,43 +1,10 @@
export type SVGProps = Record<string, string>;
/**
* A Lucide icon node (an svgson-like internal format)
*/
export type LucideIconNode =
| [attrName: string, attributes: SVGProps]
| [attrName: string, attributes: SVGProps, children: LucideIconNode[]];
/**
* A Lucide icon object that fully describes an icon to be displayed.
*/
export type LucideIconData = {
name: string;
node: LucideIconNode[];
aliases?: string[];
} & ({ size: number } | { width: number; height: number });
/**
* Build parameters for creating a Lucide icon instance for display.
*/
export type LucideBuildParams = {
/**
* The color of the icon.
*/
color?: string;
/**
* The stroke width.
*/
strokeWidth?: number;
/**
* Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements.
*/
absoluteStrokeWidth?: boolean;
/**
* Extra CSS class names to pass to the SVG element.
*/
className?: string;
/**
* Any extra attributes to pass to the SVG element.
*/
attributes?: SVGProps;
} & ({ size?: number } | { width?: number; height?: number });
import type {
LucideBuildParams as SharedLucideBuildParams,
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
SVGProps as SharedSvgProps,
} from '@lucide/shared/types';
export type LucideBuildParams = SharedLucideBuildParams;
export type LucideIconData = SharedLucideIconData;
export type LucideIconNode = SharedLucideIconNode;
export type SVGProps = SharedSvgProps;
@@ -1,3 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`buildLucideIconNode > should match the snapshot 1`] = `"{"xmlns":"http://www.w3.org/2000/svg","width":"24","height":"24","viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","class":"lucide lucide-house lucide-home"}"`;
exports[`buildLucideIconNode > should match the snapshot 1`] = `"{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round","class":"lucide lucide-house lucide-home"}"`;
@@ -18,24 +18,24 @@ describe('buildLucideIconNode', () => {
it('should override dimensions, but not viewBox', () => {
const HouseSVG = buildLucideIconNode(House, { size: 12 });
expect(HouseSVG[1]['width']).toBe('12');
expect(HouseSVG[1]['height']).toBe('12');
expect(HouseSVG[1]['width']).toBe(12);
expect(HouseSVG[1]['height']).toBe(12);
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
});
it('should override width, but not height', () => {
const HouseSVG = buildLucideIconNode(House, { width: 12 });
expect(HouseSVG[1]['width']).toBe('12');
expect(HouseSVG[1]['height']).toBe('24');
expect(HouseSVG[1]['width']).toBe(12);
expect(HouseSVG[1]['height']).toBe(24);
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
});
it('should override height, but not width', () => {
const HouseSVG = buildLucideIconNode(House, { height: 12 });
expect(HouseSVG[1]['width']).toBe('24');
expect(HouseSVG[1]['height']).toBe('12');
expect(HouseSVG[1]['width']).toBe(24);
expect(HouseSVG[1]['height']).toBe(12);
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
});
@@ -49,11 +49,31 @@ describe('buildLucideIconNode', () => {
it('should override stroke width', () => {
const HouseSVG = buildLucideIconNode(House, { strokeWidth: 12 });
expect(HouseSVG[1]['stroke-width']).toBe('12');
expect(HouseSVG[1]['stroke-width']).toBe(12);
});
it('should scale the stroke width', () => {
const HouseSVG = buildLucideIconNode(House, {
size: 12,
strokeWidth: 2,
absoluteStrokeWidth: true,
});
expect(HouseSVG[1]['stroke-width']).toBe(4);
});
it('should not scale the stroke width', () => {
const HouseSVG = buildLucideIconNode(House, {
size: 12,
strokeWidth: 2,
absoluteStrokeWidth: false,
});
expect(HouseSVG[1]['stroke-width']).toBe(2);
});
it('should set non-scaling-stroke to child nodes', () => {
const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: true });
const HouseSVG = buildLucideIconNode(House, { nonScalingStroke: true });
for (const node of HouseSVG[2]!) {
expect(node[1]['vector-effect']).toBe('non-scaling-stroke');
@@ -61,7 +81,7 @@ describe('buildLucideIconNode', () => {
});
it('should not set non-scaling-stroke', () => {
const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: false });
const HouseSVG = buildLucideIconNode(House, { nonScalingStroke: false });
expect(HouseSVG[1]['vector-effect']).toBeUndefined();
});
@@ -89,4 +109,30 @@ describe('buildLucideIconNode', () => {
expect(HouseSVG[1]['class']).toBe('lucide lucide-house lucide-home icon');
});
it('should support className with multiple classes', () => {
const HouseSVG = buildLucideIconNode(House, {
className: ' icon custom lucide-house ',
});
expect(HouseSVG[1]['class']).toBe('lucide lucide-house lucide-home icon custom');
});
it('should not include default classes when includeDefaultClasses is false', () => {
const HouseSVG = buildLucideIconNode(House, { includeDefaultClasses: false });
expect(HouseSVG[1]['class']).toBeUndefined();
});
it('should add aria-hidden when hasA11yProp is false', () => {
const HouseSVG = buildLucideIconNode(House, { hasA11yProp: false });
expect(HouseSVG[1]['aria-hidden']).toBe('true');
});
it('should not add aria-hidden when hasA11yProp is true', () => {
const HouseSVG = buildLucideIconNode(House, { hasA11yProp: true });
expect(HouseSVG[1]['aria-hidden']).toBeUndefined();
});
});
+5
View File
@@ -8,6 +8,11 @@
"moduleResolution": "node",
"module": "ESNext",
"target": "ESNext",
"baseUrl": ".",
"paths": {
"@lucide/shared": ["../shared/src/index.ts"],
"@lucide/shared/types": ["../shared/src/build/types.ts"],
},
"esModuleInterop": true,
"isolatedModules": true,
"lib": ["esnext", "dom"],
+8 -3
View File
@@ -2,6 +2,11 @@ import plugins from '@lucide/rollup-plugins';
import dts from 'rollup-plugin-dts';
import pkg from './package.json' with { type: 'json' };
const dtsOptions = {
includeExternal: ['@lucide/shared/types'],
tsconfig: './tsconfig.json',
};
const packageName = 'LucidePreact';
const outputFileName = 'lucide-preact';
const outputDir = 'dist';
@@ -58,7 +63,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: `src/${outputFileName}.suffixed.ts`,
@@ -68,7 +73,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: `src/${outputFileName}.prefixed.ts`,
@@ -78,7 +83,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
...configs,
];
@@ -1,19 +1,16 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(async ({
componentName,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
export default defineExportTemplate(
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `
return `
import createLucideIcon from '../createLucideIcon';
import type { LucideIconData } from '../types';
const iconData: LucideIconData = ${JSON.stringify(iconData)};
/**
* @component @name ${componentName}
@@ -26,8 +23,9 @@ import createLucideIcon from '../createLucideIcon';
* @returns {JSX.Element} JSX Element
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName} = createLucideIcon('${iconName}', ${JSON.stringify(children)});
const ${componentName} = createLucideIcon(iconData);
export default ${componentName};
`;
});
},
);
+41 -28
View File
@@ -1,13 +1,19 @@
import { h, toChildArray } from 'preact';
import defaultAttributes from './defaultAttributes';
import type { IconNode, LucideProps } from './types';
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
import type { LucideIconData, LucideIconNode, LucideProps } from './types';
import { useLucideContext } from './context';
import { mergeClasses } from '@lucide/shared';
import { hasA11yProp } from '@lucide/shared';
interface IconComponentProps extends LucideProps {
iconNode: IconNode;
}
type IconComponentProps = LucideProps &
(
| {
icon: LucideIconData;
iconNode?: never;
}
| {
iconNode: LucideIconNode[];
icon?: never;
}
);
/**
* Lucide icon component
@@ -18,19 +24,28 @@ interface IconComponentProps extends LucideProps {
* @param {number} props.size - The size of the icon
* @param {number} props.strokeWidth - The stroke width of the icon
* @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
* @param {boolean} props.nonScalingStroke - Whether to use non scaling strokes
* @param {string} props.class - The class name of the icon
* @param {IconNode} props.children - The children of the icon
* @param {IconNode} props.iconNode - The icon node of the icon
* @param {LucideIconNode[]} props.children - The children of the icon
* @param {LucideIconNode[]} props.iconNode - The icon node of the icon
*
* @returns {ForwardRefExoticComponent} LucideIcon
*/
const Icon = ({
color,
size,
width,
height,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
children,
iconNode,
iconNode = [],
icon = {
node: iconNode,
aliases: [],
size: 24,
},
class: classes = '',
...rest
}: IconComponentProps) => {
@@ -38,29 +53,27 @@ const Icon = ({
size: contextSize = 24,
strokeWidth: contextStrokeWidth = 2,
absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
nonScalingStroke: contextNonScalingStroke = false,
color: contextColor = 'currentColor',
class: contextClass = '',
} = useLucideContext() ?? {};
const calculatedStrokeWidth =
absoluteStrokeWidth ?? contextAbsoluteStrokeWidth
? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize)
: strokeWidth ?? contextStrokeWidth;
const [name, svgAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
color: color ?? contextColor,
width: width ?? size ?? contextSize,
height: height ?? size ?? contextSize,
strokeWidth: strokeWidth ?? contextStrokeWidth,
absoluteStrokeWidth: absoluteStrokeWidth ?? contextAbsoluteStrokeWidth,
nonScalingStroke: nonScalingStroke ?? contextNonScalingStroke,
className: mergeClasses(contextClass, classes as string),
hasA11yProp: Boolean(children) || hasA11yProp(rest),
attributes: rest,
});
return h(
'svg',
{
...defaultAttributes,
width: size ?? contextSize ?? 24,
height: size ?? contextSize ?? 24,
stroke: color ?? contextColor,
['stroke-width' as 'strokeWidth']: calculatedStrokeWidth,
class: mergeClasses('lucide', contextClass, classes),
...(!children && !hasA11yProp(rest) && { 'aria-hidden': 'true' }),
...rest,
},
[...iconNode.map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)],
);
return h(name, { ...svgAttributes }, [
...builtIconNode.map(([tag, attrs]) => h(tag, attrs)),
...toChildArray(children),
]);
};
export default Icon;
+12 -1
View File
@@ -5,13 +5,18 @@ const LucideContext = createContext<{
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
class?: string;
}>({
size: 24,
color: 'currentColor',
strokeWidth: 2,
absoluteStrokeWidth: false,
nonScalingStroke: false,
class: '',
});
@@ -20,7 +25,11 @@ interface LucideProviderProps {
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
class?: string;
}
@@ -30,6 +39,7 @@ export function LucideProvider({
color,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
class: className,
}: LucideProviderProps) {
const value = useMemo(
@@ -38,9 +48,10 @@ export function LucideProvider({
color,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
class: className,
}),
[size, color, strokeWidth, absoluteStrokeWidth, className],
[size, color, strokeWidth, absoluteStrokeWidth, nonScalingStroke, className],
);
return h(LucideContext.Provider, { value }, children);
+39 -17
View File
@@ -1,34 +1,56 @@
import { h, type JSX } from 'preact';
import { mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared';
import { h, JSX } from 'preact';
import { mergeClasses, toLucideIconData, toPascalCase } from '@lucide/shared';
import Icon from './Icon';
import type { IconNode, LucideIcon, LucideProps } from './types';
import type { LucideIcon, LucideIconData, LucideIconNode, LucideProps } from './types';
/**
* Create a Lucide icon component
* @param {string} iconName
* @param {array} iconNode
* @returns {FunctionComponent} LucideIcon
* Creates a Lucide icon component from icon data.
*
* @param icon Icon data object.
* @returns Lucide icon component.
*/
const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
function createLucideIcon(icon: LucideIconData): LucideIcon;
/**
* Creates a Lucide icon component from the legacy icon arguments.
*
* @param iconName Icon name.
* @param iconNode Icon node.
* @param aliases Optional icon aliases.
* @returns Lucide icon component.
*/
function createLucideIcon(
iconName: string,
iconNode: LucideIconNode[],
aliases?: string[],
): LucideIcon;
function createLucideIcon(
iconDataOrName: LucideIconData | string,
iconNode?: LucideIconNode[],
aliases: string[] = [],
): LucideIcon {
const iconData =
typeof iconDataOrName === 'string'
? toLucideIconData(iconDataOrName, iconNode as LucideIconNode[], aliases)
: iconDataOrName;
const Component = ({ class: classes = '', className = '', children, ...props }: LucideProps) =>
h(
Icon,
{
...props,
iconNode,
class: mergeClasses<string | JSX.SignalLike<string | undefined>>(
`lucide-${toKebabCase(toPascalCase(iconName))}`,
`lucide-${toKebabCase(iconName)}`,
classes,
className,
),
icon: iconData,
class: mergeClasses<string | JSX.SignalLike<string | undefined>>(classes, className),
},
children,
);
Component.displayName = toPascalCase(iconName);
if (iconData.name) {
Component.displayName = toPascalCase(iconData.name);
}
return Component;
};
}
export default createLucideIcon;
@@ -1,13 +1 @@
import type { JSX } from 'preact';
export default {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
} satisfies JSX.SVGAttributes;
export { defaultAttributes as default } from '@lucide/shared';
+18 -1
View File
@@ -1,12 +1,29 @@
import { type FunctionComponent, type JSX } from 'preact';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared/types';
export type IconNode = [elementName: keyof JSX.IntrinsicElements, attrs: Record<string, string>][];
export type LucideIconNode = SharedLucideIconNode;
export type LucideIconData = SharedLucideIconData;
/**
* @deprecated Use LucideIconNode instead.
*/
export type IconNode = LucideIconNode[];
export interface LucideProps extends Partial<Omit<JSX.SVGAttributes, 'ref' | 'size'>> {
color?: string;
size?: string | number;
width?: string | number;
height?: string | number;
strokeWidth?: string | number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
}
export type LucideIcon = FunctionComponent<LucideProps>;
@@ -3,7 +3,7 @@
exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-house"
class="lucide lucide-house lucide-home"
fill="none"
height="48"
stroke="red"
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using createLucideIcon > should create a component from an iconNode 1`] = `
exports[`Using createLucideIcon > should create a component from icon data 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-air-vent"
@@ -29,10 +29,10 @@ exports[`Using createLucideIcon > should create a component from an iconNode 1`]
</svg>
`;
exports[`Using createLucideIcon > should create a component from an iconNode with iconName 1`] = `
exports[`Using createLucideIcon > should include aliases from icon data 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-air-vent"
class="lucide lucide-circle-check lucide-check-circle"
fill="none"
height="24"
stroke="currentColor"
@@ -58,10 +58,10 @@ exports[`Using createLucideIcon > should create a component from an iconNode wit
</svg>
`;
exports[`Using createLucideIcon > should include backwards compatible className 1`] = `
exports[`Using createLucideIcon > should merge className prop with icon classes 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-layout2 lucide-layout-2"
class="lucide lucide-circle-check lucide-check-circle custom-class"
fill="none"
height="24"
stroke="currentColor"
@@ -10,7 +10,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid3x3 lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
@@ -31,33 +31,42 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
</svg>
`;
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid3x3 lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
<rect vector-effect="non-scaling-stroke"
width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path d="M3 9h18">
<path vector-effect="non-scaling-stroke"
d="M3 9h18"
>
</path>
<path d="M3 15h18">
<path vector-effect="non-scaling-stroke"
d="M3 15h18"
>
</path>
<path d="M9 3v18">
<path vector-effect="non-scaling-stroke"
d="M9 3v18"
>
</path>
<path d="M15 3v18">
<path vector-effect="non-scaling-stroke"
d="M15 3v18"
>
</path>
</svg>
`;
@@ -72,7 +81,38 @@ exports[`Using lucide icon components > should render an component 1`] = `
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid3x3 lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path d="M3 9h18">
</path>
<path d="M3 15h18">
</path>
<path d="M9 3v18">
</path>
<path d="M15 3v18">
</path>
</svg>
`;
exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
+20 -1
View File
@@ -51,6 +51,22 @@ describe('Using LucideProvider', () => {
expect(IconComponent).toHaveAttribute('stroke-width', '1');
});
it('should render the icon with LucideProvider and custom nonScalingStroke', () => {
const { container } = render(
<LucideProvider
size={48}
color="red"
nonScalingStroke
>
<House />
</LucideProvider>,
);
const IconComponent = container.firstElementChild;
expect(IconComponent?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
});
it("should override the provider's global props when passing props to the icon", () => {
const { container } = render(
<LucideProvider
@@ -83,6 +99,9 @@ describe('Using LucideProvider', () => {
const IconComponent = container.firstElementChild;
expect(IconComponent).toHaveAttribute('class', 'lucide provider-class lucide-house icon-class');
expect(IconComponent).toHaveAttribute(
'class',
'lucide lucide-house lucide-home provider-class icon-class',
);
});
});
@@ -4,30 +4,58 @@ import { airVent } from './testIconNodes';
import { render } from '@testing-library/preact';
describe('Using createLucideIcon', () => {
it('should create a component from an iconNode', () => {
it('should create a component from icon data', () => {
const AirVent = createLucideIcon({
name: 'air-vent',
node: airVent,
});
const { container } = render(<AirVent />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should include aliases from icon data', () => {
const CircleCheck = createLucideIcon({
name: 'circle-check',
aliases: ['check-circle'],
node: airVent,
});
const { container } = render(<CircleCheck />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should merge className prop with icon classes', () => {
const CircleCheck = createLucideIcon({
name: 'circle-check',
aliases: ['check-circle'],
node: airVent,
});
const { container } = render(<CircleCheck class="custom-class" />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should create a component from legacy icon arguments', () => {
const AirVent = createLucideIcon('AirVent', airVent);
const { container } = render(<AirVent />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
expect(container.firstChild).toHaveClass('lucide-air-vent');
});
it('should create a component from an iconNode with iconName', () => {
const AirVent = createLucideIcon('air-vent', airVent);
it('should include aliases from legacy icon arguments', () => {
const CircleCheck = createLucideIcon('CircleCheck', airVent, ['check-circle']);
const { container } = render(<AirVent />);
const { container } = render(<CircleCheck />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should include backwards compatible className', () => {
const Layout2 = createLucideIcon('layout-2', airVent);
const { container } = render(<Layout2 />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
expect(container.firstChild).toHaveClass('lucide-circle-check');
expect(container.firstChild).toHaveClass('lucide-check-circle');
});
});
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { render, cleanup } from '@testing-library/preact';
import { Pen, Edit2, Grid, Droplet } from '../src/lucide-preact';
import { Pen, Edit2, Grid, Droplet, LucideIconData, createLucideIcon } from '../src/lucide-preact';
import defaultAttributes from '../src/defaultAttributes';
type AttributesAssertion = { attributes: Record<string, { value: string }> };
@@ -70,7 +70,7 @@ describe('Using lucide icon components', () => {
expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML);
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
it('should scale the strokeWidth when absoluteStrokeWidth is set', () => {
const { container } = render(
<Grid
size={48}
@@ -89,12 +89,48 @@ describe('Using lucide icon components', () => {
expect(container.innerHTML).toMatchSnapshot();
});
it('should apply vector-effect when nonScalingStroke is set', () => {
const { container } = render(
<Grid
size={48}
stroke="red"
nonScalingStroke
/>,
);
const SVGElement = container.firstElementChild;
expect(SVGElement).toHaveAttribute('stroke', 'red');
expect(SVGElement).toHaveAttribute('width', '48');
expect(SVGElement).toHaveAttribute('height', '48');
expect(SVGElement).toHaveAttribute('stroke-width', '2');
expect(SVGElement?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
expect(container.innerHTML).toMatchSnapshot();
});
it('should apply all classes to the element', () => {
const iconData: LucideIconData = {
name: 'droplet',
size: 24,
node: [
[
'path',
{
d: 'M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z',
key: 'abc123',
},
],
],
aliases: ['drop'],
};
const testClass = 'my-class';
const { container } = render(<Droplet class={testClass} />);
const Icon = createLucideIcon(iconData);
const { container } = render(<Icon class={testClass} />);
expect(container.firstChild).toHaveClass(testClass);
expect(container.firstChild).toHaveClass('lucide');
expect(container.firstChild).toHaveClass('lucide-droplet');
expect(container.firstChild).toHaveClass('lucide-drop');
});
});
+5
View File
@@ -8,6 +8,11 @@
"moduleResolution": "node",
"module": "ESNext",
"target": "ESNext",
"baseUrl": ".",
"paths": {
"@lucide/shared": ["../shared/src/index.ts"],
"@lucide/shared/types": ["../shared/src/build/types.ts"],
},
"esModuleInterop": true,
"isolatedModules": true,
"lib": ["esnext", "dom"],
@@ -3,6 +3,11 @@ import dts from 'rollup-plugin-dts';
import pkg from './package.json' with { type: 'json' };
import getIconEntryNamesAndAliases from './scripts/getIconEntryNamesAndAliases.mts';
const dtsOptions = {
includeExternal: ['@lucide/shared/types'],
tsconfig: './tsconfig.json',
};
const packageName = 'LucideReact';
const outputFileName = 'lucide-react-native';
const outputDir = 'dist';
@@ -60,7 +65,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: 'src/icons/index.ts',
@@ -70,7 +75,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: iconAndAliasEntries,
@@ -95,7 +100,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: `src/${outputFileName}.prefixed.ts`,
@@ -105,7 +110,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
...configs,
];
@@ -1,19 +1,16 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(async ({
componentName,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
export default defineExportTemplate(
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `
return `
import createLucideIcon from '../createLucideIcon';
import type { LucideIconData } from '../types';
const iconData: LucideIconData = ${JSON.stringify(iconData)};
/**
* @component @name ${componentName}
@@ -26,8 +23,9 @@ import createLucideIcon from '../createLucideIcon';
* @returns {JSX.Element} JSX Element
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName} = createLucideIcon('${componentName}', ${JSON.stringify(children)});
const ${componentName} = createLucideIcon(iconData);
export default ${componentName};
`;
});
},
);
+63 -21
View File
@@ -1,14 +1,37 @@
import { createElement, forwardRef, type FunctionComponent } from 'react';
import * as NativeSvg from 'react-native-svg';
import defaultAttributes, { childDefaultAttributes } from './defaultAttributes';
import { IconNode, LucideProps } from './types';
import { buildLucideIconForReact, hasA11yProp, mergeClasses } from '@lucide/shared';
import { childDefaultAttributes } from './defaultAttributes';
import { LucideIconData, LucideIconNode, LucideProps } from './types';
import { useLucideContext } from './context';
interface IconComponentProps extends LucideProps {
iconNode: IconNode;
type IconComponentProps = LucideProps & {
testID?: string;
className?: string;
}
} & ({ icon: LucideIconData; iconNode?: never } | { icon?: never; iconNode: LucideIconNode[] });
const toNativeSvgAttrName = (attributeName: string) => {
if (attributeName === 'class') {
return 'className';
}
if (attributeName.startsWith('data-') || attributeName.startsWith('aria-')) {
return attributeName;
}
return attributeName.replace(/-([a-z])/g, (_, character: string) => character.toUpperCase());
};
const toNativeSvgAttributes = (attributes: Record<string, any>) => {
return Object.entries(attributes).reduce<Record<string, any>>((acc, [attributeName, value]) => {
if (value === undefined) {
return acc;
}
acc[toNativeSvgAttrName(attributeName)] = value;
return acc;
}, {});
};
/**
* Lucide icon component
@@ -20,8 +43,8 @@ interface IconComponentProps extends LucideProps {
* @param {number} props.strokeWidth - The stroke width of the icon
* @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
* @param {string} props.className - The class name of the icon
* @param {IconNode} props.children - The children of the icon
* @param {IconNode} props.iconNode - The icon node of the icon
* @param {LucideIconNode[]} props.children - The children of the icon
* @param {LucideIconNode[]} props.iconNode - The icon node of the icon
*
* @returns {ForwardRefExoticComponent} LucideIcon
*/
@@ -30,10 +53,17 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
{
color,
size,
width = size,
height = size,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
children,
iconNode,
iconNode = [],
icon = {
node: iconNode,
size: 24,
},
className,
testID,
...rest
@@ -44,17 +74,28 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
size: contextSize = 24,
strokeWidth: contextStrokeWidth = 2,
absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
nonScalingStroke: contextNonScalingStroke = false,
color: contextColor = 'currentColor',
className: contextClass = '',
} = useLucideContext() ?? {};
const calculatedStrokeWidth =
absoluteStrokeWidth ?? contextAbsoluteStrokeWidth
? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize)
: strokeWidth ?? contextStrokeWidth;
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
const [, svgAttributes, builtIconNode = []] = buildLucideIconForReact(icon, {
color: color ?? contextColor,
width: width ?? size ?? contextSize,
height: height ?? size ?? contextSize,
strokeWidth: strokeWidth ?? contextStrokeWidth,
absoluteStrokeWidth: absoluteStrokeWidth ?? contextAbsoluteStrokeWidth,
nonScalingStroke: nonScalingStroke ?? contextNonScalingStroke,
className: mergeClasses(contextClass, className),
hasA11yProp: hasAccessibleProp,
attributes: rest,
});
const customAttrs = {
stroke: color ?? contextColor ?? defaultAttributes.stroke,
strokeWidth: calculatedStrokeWidth,
stroke: svgAttributes['stroke'],
strokeWidth: svgAttributes['strokeWidth'],
...rest,
};
@@ -62,21 +103,22 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
NativeSvg.Svg as unknown as string,
{
ref,
...defaultAttributes,
width: size ?? contextSize ?? defaultAttributes.width,
height: size ?? contextSize ?? defaultAttributes.height,
...toNativeSvgAttributes(svgAttributes),
'data-testid': testID,
className,
...customAttrs,
...rest,
},
[
...iconNode.map(([tag, attrs]) => {
...builtIconNode.map(([tag, attrs]) => {
const upperCasedTag = (tag.charAt(0).toUpperCase() +
tag.slice(1)) as keyof typeof NativeSvg;
// duplicating the attributes here because generating the OTA update bundles don't inherit the SVG properties from parent (codepush, expo-updates)
return createElement(
NativeSvg[upperCasedTag] as FunctionComponent<LucideProps>,
{ ...childDefaultAttributes, ...customAttrs, ...attrs } as LucideProps,
toNativeSvgAttributes({
...childDefaultAttributes,
...customAttrs,
...attrs,
}) as LucideProps,
);
}),
...((Array.isArray(children) ? children : [children]) || []),
+12 -1
View File
@@ -4,12 +4,17 @@ const LucideContext = createContext<{
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
}>({
size: 24,
color: 'currentColor',
strokeWidth: 2,
absoluteStrokeWidth: false,
nonScalingStroke: false,
});
interface LucideProviderProps {
@@ -17,7 +22,11 @@ interface LucideProviderProps {
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
}
export function LucideProvider({
@@ -26,6 +35,7 @@ export function LucideProvider({
color,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
}: LucideProviderProps) {
const value = useMemo(
() => ({
@@ -33,8 +43,9 @@ export function LucideProvider({
color,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
}),
[size, color, strokeWidth, absoluteStrokeWidth],
[size, color, strokeWidth, absoluteStrokeWidth, nonScalingStroke],
);
return createElement(LucideContext.Provider, { value }, children);
@@ -1,26 +1,53 @@
import { forwardRef, createElement } from 'react';
import { IconNode, LucideProps } from './types';
import { toPascalCase } from '@lucide/shared';
import { LucideIcon, LucideIconData, LucideIconNode, LucideProps } from './types';
import { toLucideIconData, toPascalCase } from '@lucide/shared';
import Icon from './Icon';
/**
* Create a Lucide icon component
* @param {string} iconName
* @param {array} iconNode
* @returns {ForwardRefExoticComponent} LucideIcon
* Creates a Lucide icon component from icon data.
*
* @param icon Icon data object.
* @returns Lucide icon component.
*/
const createLucideIcon = (iconName: string, iconNode: IconNode) => {
function createLucideIcon(icon: LucideIconData): LucideIcon;
/**
* Creates a Lucide icon component from the legacy icon arguments.
*
* @param iconName Icon name.
* @param iconNode Icon node.
* @param aliases Optional icon aliases.
* @returns Lucide icon component.
*/
function createLucideIcon(
iconName: string,
iconNode: LucideIconNode[],
aliases?: string[],
): LucideIcon;
function createLucideIcon(
iconDataOrName: LucideIconData | string,
iconNode?: LucideIconNode[],
aliases: string[] = [],
): LucideIcon {
const iconData =
typeof iconDataOrName === 'string'
? toLucideIconData(iconDataOrName, iconNode as LucideIconNode[], aliases)
: iconDataOrName;
const Component = forwardRef<SVGSVGElement, LucideProps>((props, ref) =>
createElement(Icon, {
ref,
iconNode,
icon: iconData,
...props,
}),
);
Component.displayName = toPascalCase(iconName);
if (iconData.name) {
Component.displayName = toPascalCase(iconData.name);
}
return Component;
};
}
export default createLucideIcon;
@@ -1,21 +1,10 @@
const defaultAttributes = {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
};
import { defaultReactAttributes } from '@lucide/shared';
export const childDefaultAttributes = {
fill: defaultAttributes.fill,
stroke: defaultAttributes.stroke,
strokeWidth: defaultAttributes.strokeWidth,
strokeLinecap: defaultAttributes.strokeLinecap,
strokeLinejoin: defaultAttributes.strokeLinejoin,
fill: defaultReactAttributes.fill,
stroke: defaultReactAttributes.stroke,
strokeWidth: defaultReactAttributes.strokeWidth,
strokeLinecap: defaultReactAttributes.strokeLinecap,
strokeLinejoin: defaultReactAttributes.strokeLinejoin,
};
export default defaultAttributes;
export default defaultReactAttributes;
+16 -14
View File
@@ -1,26 +1,28 @@
import type { ForwardRefExoticComponent } from 'react';
import type { SvgProps } from 'react-native-svg';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared/types';
export type LucideIconNode = SharedLucideIconNode;
export type LucideIconData = SharedLucideIconData;
/**
* A reduced version of `SVGElementType` from @types/react. This type was added
* with the release of React 19, and is included here in order to support usage
* with older versions.
* @deprecated Use LucideIconNode instead.
*/
type SVGElementType =
| 'circle'
| 'ellipse'
| 'g'
| 'line'
| 'path'
| 'polygon'
| 'polyline'
| 'rect';
export type IconNode = [elementName: SVGElementType, attrs: Record<string, string>][];
export type IconNode = LucideIconNode[];
export interface LucideProps extends SvgProps {
size?: string | number;
width?: string | number;
height?: string | number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
'data-testid'?: string;
}
@@ -7,6 +7,8 @@ import { Icon } from '../src/lucide-react-native';
vi.mock('react-native-svg');
describe('Using Icon Component', () => {
const airVentIcon = { name: 'air-vent', node: airVent };
it('should render icon based on a iconNode', async () => {
const { container } = render(
<Icon
@@ -32,4 +34,30 @@ describe('Using Icon Component', () => {
expect(container.firstChild).toMatchSnapshot();
});
it('should render icon based on icon data', async () => {
const { container } = render(
<Icon
icon={airVentIcon}
size={48}
stroke="red"
/>,
);
expect(container.firstChild).toBeDefined();
});
it('should support nonScalingStroke', async () => {
const { container } = render(
<Icon
icon={airVentIcon}
size={48}
stroke="red"
strokeWidth={2}
nonScalingStroke
/>,
);
expect(container.firstChild?.firstChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
});
});
@@ -2,6 +2,8 @@
exports[`Using Icon Component > should render icon and match snapshot 1`] = `
<svg
aria-hidden="true"
class="lucide"
fill="none"
height="48"
stroke="red"
@@ -2,6 +2,8 @@
exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-house lucide-home"
fill="none"
height="24"
stroke="currentColor"
@@ -2,6 +2,8 @@
exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
data-testid="grid-icon"
fill="none"
height="48"
@@ -60,6 +62,71 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
</svg>
`;
exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect fill="none"
stroke="red"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path fill="none"
stroke="red"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
d="M3 9h18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
d="M3 15h18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
d="M9 3v18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
d="M15 3v18"
>
</path>
</svg>
`;
exports[`Using lucide icon components > should duplicate properties to children components 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
@@ -70,6 +137,8 @@ exports[`Using lucide icon components > should duplicate properties to children
stroke-width="10"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
data-testid="multiple-children"
>
<rect fill="red"
@@ -119,64 +188,6 @@ exports[`Using lucide icon components > should duplicate properties to children
</svg>
`;
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M3 9h18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M3 15h18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M9 3v18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M15 3v18"
>
</path>
</svg>
`;
exports[`Using lucide icon components > should render an component 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
@@ -187,6 +198,8 @@ exports[`Using lucide icon components > should render an component 1`] = `
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect fill="none"
stroke="currentColor"
@@ -235,6 +248,66 @@ exports[`Using lucide icon components > should render an component 1`] = `
</svg>
`;
exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M3 9h18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M3 15h18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M9 3v18"
>
</path>
<path fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
d="M15 3v18"
>
</path>
</svg>
`;
exports[`Using lucide icon components > should work with a single child component 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
@@ -245,6 +318,7 @@ exports[`Using lucide icon components > should work with a single child componen
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
data-testid="single-child"
>
<rect fill="none"
@@ -300,6 +374,8 @@ exports[`Using lucide icon components > should work with a single child componen
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
data-testid="child"
>
<rect fill="none"
@@ -360,6 +436,7 @@ exports[`Using lucide icon components > should work with several children compon
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
data-testid="multiple-children"
>
<rect fill="none"
@@ -415,6 +492,8 @@ exports[`Using lucide icon components > should work with several children compon
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
data-testid="child1"
>
<rect fill="none"
@@ -471,6 +550,8 @@ exports[`Using lucide icon components > should work with several children compon
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
data-testid="child2"
>
<rect fill="none"
@@ -63,7 +63,7 @@ describe('Using lucide icon components', () => {
expect(penIcon).toStrictEqual(edit2Icon);
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
it('should scale the strokeWidth when absoluteStrokeWidth is set', () => {
const { container } = render(
<Grid
size={48}
@@ -82,6 +82,26 @@ describe('Using lucide icon components', () => {
expect(container.innerHTML).toMatchSnapshot();
});
it('should apply vector-effect when nonScalingStroke is set', () => {
const { container } = render(
<Grid
size={48}
stroke="red"
nonScalingStroke
/>,
);
const SVGElement = container.firstElementChild;
expect(SVGElement).toHaveAttribute('stroke', 'red');
expect(SVGElement).toHaveAttribute('width', '48');
expect(SVGElement).toHaveAttribute('height', '48');
expect(SVGElement).toHaveAttribute('stroke-width', '2');
expect(SVGElement?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
expect(container.innerHTML).toMatchSnapshot();
});
it('should work with a single child component', () => {
const testId = 'single-child';
const childId = 'child';
@@ -9,6 +9,11 @@
"isolatedModules": true,
"strict": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@lucide/shared": ["../shared/src/index.ts"],
"@lucide/shared/types": ["../shared/src/build/types.ts"],
},
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
+5 -4
View File
@@ -53,10 +53,11 @@
"@lucide/build-icons": "workspace:*",
"@lucide/rollup-plugins": "workspace:*",
"@lucide/shared": "workspace:*",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^14.3.1",
"@types/react": "^18.3.31",
"@vitejs/plugin-react": "^4.7.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^14.1.2",
"@types/node": "^25.9.1",
"@types/react": "^18.2.37",
"@vitejs/plugin-react": "^4.6.0",
"jest-serializer-html": "^7.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
+10 -5
View File
@@ -4,6 +4,11 @@ import pkg from './package.json' with { type: 'json' };
import dts from 'rollup-plugin-dts';
import getAliasesEntryNames from './scripts/getAliasesEntryNames.mts';
const dtsOptions = {
includeExternal: ['@lucide/shared/types'],
tsconfig: './tsconfig.json',
};
const aliasesEntries = await getAliasesEntryNames();
const packageName = 'LucideReact';
@@ -117,7 +122,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: 'src/dynamic.ts',
@@ -132,7 +137,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: inputs[0],
@@ -142,7 +147,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: `src/${outputFileName}.suffixed.ts`,
@@ -152,7 +157,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
{
input: `src/${outputFileName}.prefixed.ts`,
@@ -162,7 +167,7 @@ export default [
format: 'es',
},
],
plugins: [dts()],
plugins: [dts(dtsOptions)],
},
...configs,
];
@@ -1,22 +1,21 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(async({
componentName,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
export default defineExportTemplate(
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `
return `
import createLucideIcon from '../createLucideIcon';
import { IconNode } from '../types';
import type { LucideIconNode, LucideIconData } from '../types';
export const __iconNode: IconNode = ${JSON.stringify(children)}
export const __iconData: LucideIconData = ${JSON.stringify(iconData)}
/**
* @deprecated Access \`__iconData\` instead.
*/
export const __iconNode: LucideIconNode[] = __iconData.node;
/**
* @component @name ${componentName}
@@ -29,8 +28,9 @@ export const __iconNode: IconNode = ${JSON.stringify(children)}
* @returns {JSX.Element} JSX Element
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName} = createLucideIcon('${iconName}', __iconNode);
const ${componentName} = createLucideIcon(__iconData);
export default ${componentName};
`;
});
},
);
+19 -11
View File
@@ -1,11 +1,14 @@
'use client';
import { createElement, forwardRef, useEffect, useState } from 'react';
import { IconNode, LucideIcon, LucideProps } from './types';
import { LucideIcon, LucideIconData, LucideProps } from './types';
import dynamicIconImports from './dynamicIconImports';
import Icon from './Icon';
export type DynamicIconModule = { default: LucideIcon; __iconNode: IconNode };
export type DynamicIconModule = {
default: LucideIcon;
__iconData?: LucideIconData;
};
export type IconName = keyof typeof dynamicIconImports;
@@ -16,7 +19,7 @@ interface DynamicIconComponentProps extends LucideProps {
fallback?: () => JSX.Element | null;
}
async function getIconNode(name: IconName) {
async function getIconData(name: IconName) {
if (!(name in dynamicIconImports)) {
throw new Error('[lucide-react]: Name in Lucide DynamicIcon not found');
}
@@ -24,7 +27,11 @@ async function getIconNode(name: IconName) {
// TODO: Replace this with a generic iconNode package.
const icon = (await dynamicIconImports[name]()) as DynamicIconModule;
return icon.__iconNode;
if (icon.__iconData != null) {
return icon.__iconData;
}
throw new Error('[lucide-react]: Failed to resolve icon data for DynamicIcon');
}
/**
@@ -36,25 +43,26 @@ async function getIconNode(name: IconName) {
* @param {number} props.size - The size of the icon
* @param {number} props.strokeWidth - The stroke width of the icon
* @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
* @param {boolean} props.nonScalingStroke - Whether to use non scaling strokes
* @param {string} props.className - The class name of the icon
* @param {IconNode} props.children - The children of the icon
* @param {IconNode} props.iconNode - The icon node of the icon
* @param {LucideIconNode[]} props.children - The children of the icon
* @param {LucideIconNode[]} props.iconNode - The icon node of the icon
*
* @returns {ForwardRefExoticComponent} LucideIcon
*/
const DynamicIcon = forwardRef<SVGSVGElement, DynamicIconComponentProps>(
({ name, fallback: Fallback, ...props }, ref) => {
const [iconNode, setIconNode] = useState<IconNode>();
const [iconData, setIconData] = useState<LucideIconData>();
useEffect(() => {
getIconNode(name)
.then(setIconNode)
getIconData(name)
.then(setIconData)
.catch((error) => {
console.error(error);
});
}, [name]);
if (iconNode == null) {
if (iconData == null) {
if (Fallback == null) {
return null;
}
@@ -65,7 +73,7 @@ const DynamicIcon = forwardRef<SVGSVGElement, DynamicIconComponentProps>(
return createElement(Icon, {
ref,
...props,
iconNode,
icon: iconData,
});
},
);
+51 -23
View File
@@ -1,14 +1,21 @@
'use client';
import { createElement, forwardRef } from 'react';
import defaultAttributes from './defaultAttributes';
import { IconNode, LucideProps } from './types';
import { mergeClasses, hasA11yProp } from '@lucide/shared';
import { buildLucideIconForReact, hasA11yProp, mergeClasses } from '@lucide/shared';
import { LucideIconData, LucideIconNode, LucideProps } from './types';
import { useLucideContext } from './context';
interface IconComponentProps extends LucideProps {
iconNode: IconNode;
}
type IconComponentProps = LucideProps &
(
| {
iconNode: LucideIconNode[];
icon?: never;
}
| {
icon: LucideIconData;
iconNode?: never;
}
);
/**
* Lucide icon component
@@ -19,45 +26,66 @@ interface IconComponentProps extends LucideProps {
* @param {number} props.size - The size of the icon
* @param {number} props.strokeWidth - The stroke width of the icon
* @param {boolean} props.absoluteStrokeWidth - Whether to use absolute stroke width
* @param {boolean} props.nonScalingStroke - Whether to use non scaling strokes
* @param {string} props.className - The class name of the icon
* @param {IconNode} props.children - The children of the icon
* @param {IconNode} props.iconNode - The icon node of the icon
* @param {LucideIconNode[]} props.children - The children of the icon
* @param {LucideIconNode[]} props.iconNode - The icon node of the icon
*
* @returns {ForwardRefExoticComponent} LucideIcon
*/
const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
(
{ color, size, strokeWidth, absoluteStrokeWidth, className = '', children, iconNode, ...rest },
{
color,
size,
width,
height,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
className = '',
children,
iconNode = [],
icon = {
node: iconNode,
aliases: [],
size: 24,
},
...rest
},
ref,
) => {
const {
size: contextSize = 24,
strokeWidth: contextStrokeWidth = 2,
absoluteStrokeWidth: contextAbsoluteStrokeWidth = false,
nonScalingStroke: contextNonScalingStroke = false,
color: contextColor = 'currentColor',
className: contextClass = '',
} = useLucideContext() ?? {};
const calculatedStrokeWidth =
absoluteStrokeWidth ?? contextAbsoluteStrokeWidth
? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize)
: strokeWidth ?? contextStrokeWidth;
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
const [name, svgAttributes, builtIconNode = []] = buildLucideIconForReact(icon, {
color: color ?? contextColor,
width: width ?? size ?? contextSize,
height: height ?? size ?? contextSize,
strokeWidth: strokeWidth ?? contextStrokeWidth,
absoluteStrokeWidth: absoluteStrokeWidth ?? contextAbsoluteStrokeWidth,
nonScalingStroke: nonScalingStroke ?? contextNonScalingStroke,
className: mergeClasses(contextClass, className),
hasA11yProp: hasAccessibleProp,
attributes: rest,
});
return createElement(
'svg',
name,
{
ref,
...defaultAttributes,
width: size ?? contextSize ?? defaultAttributes.width,
height: size ?? contextSize ?? defaultAttributes.height,
stroke: color ?? contextColor,
strokeWidth: calculatedStrokeWidth,
className: mergeClasses('lucide', contextClass, className),
...(!children && !hasA11yProp(rest) && { 'aria-hidden': 'true' }),
...rest,
...svgAttributes,
},
[
...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
...builtIconNode.map(([tag, attrs]) => createElement(tag, attrs)),
...(Array.isArray(children) ? children : [children]),
],
);
+7 -1
View File
@@ -7,7 +7,11 @@ type LucideConfig = {
size: number;
color: string;
strokeWidth: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth: boolean;
nonScalingStroke: boolean;
className: string;
};
@@ -23,6 +27,7 @@ export function LucideProvider({
color,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
className,
}: LucideProviderProps) {
const value = useMemo(
@@ -31,9 +36,10 @@ export function LucideProvider({
color,
strokeWidth,
absoluteStrokeWidth,
nonScalingStroke,
className,
}),
[size, color, strokeWidth, absoluteStrokeWidth, className],
[size, color, strokeWidth, absoluteStrokeWidth, nonScalingStroke, className],
);
return createElement(LucideContext.Provider, { value }, children);
+38 -15
View File
@@ -1,31 +1,54 @@
import { createElement, forwardRef } from 'react';
import { mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared';
import { IconNode, LucideProps } from './types';
import { toLucideIconData, toPascalCase } from '@lucide/shared';
import { LucideIcon, LucideIconData, LucideIconNode, LucideProps } from './types';
import Icon from './Icon';
/**
* Create a Lucide icon component
* @param {string} iconName
* @param {array} iconNode
* @returns {ForwardRefExoticComponent} LucideIcon
* Creates a Lucide icon component from icon data.
*
* @param icon Icon data object.
* @returns Lucide icon component.
*/
const createLucideIcon = (iconName: string, iconNode: IconNode) => {
function createLucideIcon(icon: LucideIconData): LucideIcon;
/**
* Creates a Lucide icon component from the legacy icon arguments.
*
* @param iconName Icon name.
* @param iconNode Icon node.
* @param aliases Optional icon aliases.
* @returns Lucide icon component.
*/
function createLucideIcon(
iconName: string,
iconNode: LucideIconNode[],
aliases?: string[],
): LucideIcon;
function createLucideIcon(
iconDataOrName: LucideIconData | string,
iconNode: LucideIconNode[] = [],
aliases: string[] = [],
): LucideIcon {
const iconData =
typeof iconDataOrName === 'string'
? toLucideIconData(iconDataOrName, iconNode, aliases)
: iconDataOrName;
const Component = forwardRef<SVGSVGElement, LucideProps>(({ className, ...props }, ref) =>
createElement(Icon, {
ref,
iconNode,
className: mergeClasses(
`lucide-${toKebabCase(toPascalCase(iconName))}`,
`lucide-${iconName}`,
className,
),
icon: iconData,
className,
...props,
}),
);
Component.displayName = toPascalCase(iconName);
if (iconData.name) {
Component.displayName = toPascalCase(iconData.name);
}
return Component;
};
}
export default createLucideIcon;
+1 -11
View File
@@ -1,11 +1 @@
export default {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
};
export { defaultReactAttributes as default } from '@lucide/shared';
+18 -17
View File
@@ -1,28 +1,29 @@
import type { SVGProps, ForwardRefExoticComponent, RefAttributes } from 'react';
/**
* A reduced version of `SVGElementType` from @types/react. This type was added
* with the release of React 19, and is included here in order to support usage
* with older versions.
*/
type SVGElementType =
| 'circle'
| 'ellipse'
| 'g'
| 'line'
| 'path'
| 'polygon'
| 'polyline'
| 'rect';
export type IconNode = [elementName: SVGElementType, attrs: Record<string, string>][];
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared/types';
export type SVGAttributes = Partial<SVGProps<SVGSVGElement>>;
export type LucideIconNode = SharedLucideIconNode<string, SVGAttributes>;
export type LucideIconData = SharedLucideIconData<string, SVGAttributes>;
/**
* @deprecated Use LucideIconNode instead.
*/
export type IconNode = LucideIconNode[];
type ElementAttributes = RefAttributes<SVGSVGElement> & SVGAttributes;
export interface LucideProps extends ElementAttributes {
size?: string | number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
}
export type LucideIcon = ForwardRefExoticComponent<
@@ -3,7 +3,7 @@
exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-house"
class="lucide lucide-house lucide-home"
fill="none"
height="48"
stroke="red"
@@ -22,3 +22,51 @@ exports[`Using LucideProvider > should render the icon with LucideProvider 1`] =
/>
</svg>
`;
exports[`Using LucideProvider > should render the icon with LucideProvider and custom absoluteStrokeWidth 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-house lucide-home"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"
/>
<path
d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
/>
</svg>
`;
exports[`Using LucideProvider > should render the icon with LucideProvider and custom nonScalingStroke 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-house lucide-home"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"
vector-effect="non-scaling-stroke"
/>
<path
d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
vector-effect="non-scaling-stroke"
/>
</svg>
`;
@@ -1,35 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using createLucideIcon > should create a component from an iconNode 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-air-vent lucide-AirVent"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
/>
<path
d="M6 8h12"
/>
<path
d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"
/>
<path
d="M6.6 15.6A2 2 0 1 0 10 17v-5"
/>
</svg>
`;
exports[`Using createLucideIcon > should create a component from an iconNode with iconName 1`] = `
exports[`Using createLucideIcon > should create a component from icon data 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-air-vent"
@@ -58,10 +29,39 @@ exports[`Using createLucideIcon > should create a component from an iconNode wit
</svg>
`;
exports[`Using createLucideIcon > should include backwards compatible className 1`] = `
exports[`Using createLucideIcon > should include aliases from icon data 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-layout2 lucide-layout-2"
class="lucide lucide-circle-check lucide-check-circle"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
/>
<path
d="M6 8h12"
/>
<path
d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"
/>
<path
d="M6.6 15.6A2 2 0 1 0 10 17v-5"
/>
</svg>
`;
exports[`Using createLucideIcon > should merge className prop with icon classes 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-circle-check lucide-check-circle custom-class"
fill="none"
height="24"
stroke="currentColor"
@@ -10,7 +10,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid3x3 lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
@@ -31,33 +31,42 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
</svg>
`;
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid3x3 lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
<rect vector-effect="non-scaling-stroke"
width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path d="M3 9h18">
<path vector-effect="non-scaling-stroke"
d="M3 9h18"
>
</path>
<path d="M3 15h18">
<path vector-effect="non-scaling-stroke"
d="M3 15h18"
>
</path>
<path d="M9 3v18">
<path vector-effect="non-scaling-stroke"
d="M9 3v18"
>
</path>
<path d="M15 3v18">
<path vector-effect="non-scaling-stroke"
d="M15 3v18"
>
</path>
</svg>
`;
@@ -72,7 +81,38 @@ exports[`Using lucide icon components > should render an component 1`] = `
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid3x3 lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
>
</rect>
<path d="M3 9h18">
</path>
<path d="M3 15h18">
</path>
<path d="M9 3v18">
</path>
<path d="M15 3v18">
</path>
</svg>
`;
exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
aria-hidden="true"
>
<rect width="18"
+22 -1
View File
@@ -60,6 +60,24 @@ describe('Using LucideProvider', () => {
const IconComponent = container.firstElementChild;
expect(IconComponent).toHaveAttribute('stroke-width', '1');
expect(container.firstChild).toMatchSnapshot();
});
it('should render the icon with LucideProvider and custom nonScalingStroke', () => {
const { container } = render(
<LucideProvider
size={48}
color="red"
nonScalingStroke
>
<House />
</LucideProvider>,
);
const IconComponent = container.firstElementChild;
expect(IconComponent?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
expect(container.firstChild).toMatchSnapshot();
});
it("should override the provider's global props when passing props to the icon", () => {
@@ -94,6 +112,9 @@ describe('Using LucideProvider', () => {
const IconComponent = container.firstElementChild;
expect(IconComponent).toHaveAttribute('class', 'lucide provider-class lucide-house icon-class');
expect(IconComponent).toHaveAttribute(
'class',
'lucide lucide-house lucide-home provider-class icon-class',
);
});
});
@@ -4,30 +4,58 @@ import { airVent } from './testIconNodes';
import { render } from '@testing-library/react';
describe('Using createLucideIcon', () => {
it('should create a component from an iconNode', () => {
it('should create a component from icon data', () => {
const AirVent = createLucideIcon({
name: 'air-vent',
node: airVent,
});
const { container } = render(<AirVent />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should include aliases from icon data', () => {
const CircleCheck = createLucideIcon({
name: 'circle-check',
aliases: ['check-circle'],
node: airVent,
});
const { container } = render(<CircleCheck />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should merge className prop with icon classes', () => {
const CircleCheck = createLucideIcon({
name: 'circle-check',
aliases: ['check-circle'],
node: airVent,
});
const { container } = render(<CircleCheck className="custom-class" />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should create a component from legacy icon arguments', () => {
const AirVent = createLucideIcon('AirVent', airVent);
const { container } = render(<AirVent />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
expect(container.firstChild).toHaveClass('lucide-air-vent');
});
it('should create a component from an iconNode with iconName', () => {
const AirVent = createLucideIcon('air-vent', airVent);
it('should include aliases from legacy icon arguments', () => {
const CircleCheck = createLucideIcon('CircleCheck', airVent, ['check-circle']);
const { container } = render(<AirVent />);
const { container } = render(<CircleCheck />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
});
it('should include backwards compatible className', () => {
const Layout2 = createLucideIcon('layout-2', airVent);
const { container } = render(<Layout2 />);
expect(container.firstChild).toMatchSnapshot();
expect(container.firstChild).toBeDefined();
expect(container.firstChild).toHaveClass('lucide-circle-check');
expect(container.firstChild).toHaveClass('lucide-check-circle');
});
});
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { render, cleanup } from '@testing-library/react';
import { Pen, Edit2, Grid, Droplet } from '../src/lucide-react';
import { Pen, Edit2, Grid, createLucideIcon, LucideIconData } from '../src/lucide-react';
import defaultAttributes from '../src/defaultAttributes';
describe('Using lucide icon components', () => {
@@ -69,7 +69,7 @@ describe('Using lucide icon components', () => {
expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML);
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
it('should scale the strokeWidth when absoluteStrokeWidth is set', () => {
const { container, getByTestId } = render(
<Grid
size={48}
@@ -88,12 +88,48 @@ describe('Using lucide icon components', () => {
expect(container.innerHTML).toMatchSnapshot();
});
it('should apply vector-effect when nonScalingStroke is set', () => {
const { container, getByTestId } = render(
<Grid
size={48}
stroke="red"
nonScalingStroke
/>,
);
const SVGElement = container.firstElementChild;
expect(SVGElement).toHaveAttribute('stroke', 'red');
expect(SVGElement).toHaveAttribute('width', '48');
expect(SVGElement).toHaveAttribute('height', '48');
expect(SVGElement).toHaveAttribute('stroke-width', '2');
expect(SVGElement?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
expect(container.innerHTML).toMatchSnapshot();
});
it('should apply all classNames to the element', () => {
const iconData: LucideIconData = {
name: 'droplet',
size: 24,
node: [
[
'path',
{
d: 'M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z',
key: 'abc123',
},
],
],
aliases: ['drop'],
};
const testClass = 'my-class';
const { container } = render(<Droplet className={testClass} />);
const Icon = createLucideIcon(iconData);
const { container } = render(<Icon className={testClass} />);
expect(container.firstChild).toHaveClass(testClass);
expect(container.firstChild).toHaveClass('lucide');
expect(container.firstChild).toHaveClass('lucide-droplet');
expect(container.firstChild).toHaveClass('lucide-drop');
});
});
+6 -1
View File
@@ -8,6 +8,11 @@
"moduleResolution": "node",
"module": "ESNext",
"target": "ESNext",
"baseUrl": ".",
"paths": {
"@lucide/shared": ["../shared/src/index.ts"],
"@lucide/shared/types": ["../shared/src/build/types.ts"],
},
"esModuleInterop": true,
"isolatedModules": true,
"lib": ["esnext", "dom"],
@@ -19,7 +24,7 @@
"sourceMap": true,
"outDir": "./dist",
"jsx": "react-jsx",
"types": ["@testing-library/jest-dom"],
"types": ["@testing-library/jest-dom", "node"],
},
"exclude": ["**/node_modules"],
"include": ["src", "tests"],
@@ -1,22 +1,16 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(async ({
componentName,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
export default defineExportTemplate(
async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `
return `
import Icon from '../Icon';
import type { IconNode, LucideProps } from '../types';
import type { LucideIconData, LucideProps } from '../types';
const iconNode: IconNode = ${JSON.stringify(children)};
const iconData: LucideIconData = ${JSON.stringify(iconData)};
/**
* @component @name ${componentName}
@@ -30,9 +24,10 @@ const iconNode: IconNode = ${JSON.stringify(children)};
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName} = (props: LucideProps) => (
<Icon {...props} iconNode={iconNode} name="${iconName}" />
<Icon {...props} icon={iconData} />
)
export default ${componentName};
`;
});
},
);
+37 -44
View File
@@ -1,66 +1,59 @@
import { For, splitProps, useContext } from 'solid-js';
import { Dynamic } from 'solid-js/web';
import defaultAttributes from './defaultAttributes';
import { IconNode, LucideProps } from './types';
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
import { LucideIconData, LucideIconNode, LucideProps } from './types';
import { LucideContext } from './context';
import { hasA11yProp, mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared';
interface IconProps {
name?: string;
iconNode: IconNode;
}
type IconProps =
| {
icon: LucideIconData;
iconNode?: never;
}
| {
icon?: never;
iconNode?: LucideIconNode[];
};
const Icon = (props: LucideProps & IconProps) => {
const [localProps, rest] = splitProps(props, [
'color',
'size',
'width',
'height',
'strokeWidth',
'children',
'class',
'name',
'icon',
'iconNode',
'absoluteStrokeWidth',
'nonScalingStroke',
]);
const globalProps = useContext(LucideContext);
const hasAccessibleProp = Boolean(localProps.children) || hasA11yProp(rest);
const icon: LucideIconData = localProps.icon ?? {
node: localProps.iconNode ?? ([] as LucideIconNode[]),
size: 24,
aliases: [],
};
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
color: localProps.color ?? globalProps.color,
width: localProps.width ?? localProps.size ?? globalProps.size,
height: localProps.height ?? localProps.size ?? globalProps.size,
strokeWidth: localProps.strokeWidth ?? globalProps.strokeWidth,
absoluteStrokeWidth: localProps.absoluteStrokeWidth ?? globalProps.absoluteStrokeWidth,
nonScalingStroke: localProps.nonScalingStroke ?? globalProps.nonScalingStroke,
className: mergeClasses('lucide-icon', globalProps.class, localProps.class),
hasA11yProp: hasAccessibleProp,
attributes: rest,
});
return (
<svg
{...defaultAttributes}
width={localProps.size ?? globalProps.size ?? defaultAttributes.width}
height={localProps.size ?? globalProps.size ?? defaultAttributes.height}
stroke={localProps.color ?? globalProps.color ?? defaultAttributes.stroke}
stroke-width={
(localProps.absoluteStrokeWidth ?? globalProps.absoluteStrokeWidth) === true
? (Number(
localProps.strokeWidth ??
globalProps.strokeWidth ??
defaultAttributes['stroke-width'],
) *
24) /
Number(localProps.size ?? globalProps.size)
: Number(
localProps.strokeWidth ??
globalProps.strokeWidth ??
defaultAttributes['stroke-width'],
)
}
class={mergeClasses(
'lucide',
'lucide-icon',
globalProps.class,
...(localProps.name != null
? [
`lucide-${toKebabCase(toPascalCase(localProps.name))}`,
`lucide-${toKebabCase(localProps.name)}`,
]
: []),
localProps.class,
)}
aria-hidden={!localProps.children && !hasA11yProp(rest) ? 'true' : undefined}
{...rest}
>
<For each={localProps.iconNode}>
<svg {...svgAttributes}>
<For each={builtIconNode}>
{([elementName, attrs]) => {
return (
<Dynamic
+10
View File
@@ -4,13 +4,18 @@ export const LucideContext = createContext<{
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
class?: string;
}>({
size: 24,
color: 'currentColor',
strokeWidth: 2,
absoluteStrokeWidth: false,
nonScalingStroke: false,
class: '',
});
@@ -19,7 +24,11 @@ interface LucideProviderProps {
size?: number;
color?: string;
strokeWidth?: number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
class?: string;
}
@@ -29,6 +38,7 @@ export function LucideProvider(props: LucideProviderProps) {
'color',
'strokeWidth',
'absoluteStrokeWidth',
'nonScalingStroke',
'class',
]);
+1 -15
View File
@@ -1,15 +1 @@
import { SVGAttributes } from './types';
const defaultAttributes: SVGAttributes = {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
};
export default defaultAttributes;
export { defaultAttributes as default } from '@lucide/shared';
+19 -1
View File
@@ -1,15 +1,33 @@
import { JSX } from 'solid-js/jsx-runtime';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared/types';
export type IconNode = [elementName: keyof JSX.IntrinsicElements, attrs: Record<string, string>][];
export type SVGAttributes = Partial<JSX.SvgSVGAttributes<SVGSVGElement>>;
export type LucideIconNode = SharedLucideIconNode<keyof JSX.IntrinsicElements, SVGAttributes>;
export type LucideIconData = SharedLucideIconData<keyof JSX.IntrinsicElements, SVGAttributes>;
/**
* @deprecated Use LucideIconNode instead.
*/
export type IconNode = LucideIconNode[];
export interface LucideProps extends SVGAttributes {
key?: string | number;
class?: string;
size?: string | number;
width?: string | number;
height?: string | number;
color?: string;
strokeWidth?: string | number;
/**
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
nonScalingStroke?: boolean;
}
export type LucideIcon = (props: LucideProps) => JSX.Element;
+28
View File
@@ -5,6 +5,8 @@ import { airVent } from './testIconNodes';
import { Icon } from '../src/lucide-solid';
describe('Using Icon Component', () => {
const airVentIcon = { name: 'air-vent', node: airVent };
it('should render icon based on a iconNode', async () => {
const { container } = render(() => (
<Icon
@@ -30,6 +32,32 @@ describe('Using Icon Component', () => {
expect(container.firstChild).toMatchSnapshot();
});
it('should render icon based on icon data', async () => {
const { container } = render(() => (
<Icon
icon={airVentIcon}
size={48}
stroke="red"
/>
));
expect(container.firstChild).toBeDefined();
});
it('should support nonScalingStroke', async () => {
const { container } = render(() => (
<Icon
icon={airVentIcon}
size={48}
stroke="red"
strokeWidth={2}
nonScalingStroke
/>
));
expect(container.firstChild?.firstChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
});
});
describe('Icon Component Accessibility', () => {
@@ -3,7 +3,7 @@
exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = `
<svg
aria-hidden="true"
class="lucide lucide-icon lucide-house"
class="lucide lucide-house lucide-home lucide-icon"
fill="none"
height="48"
stroke="red"
@@ -2,56 +2,15 @@
exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 24 24"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="4"
class="lucide lucide-icon lucide-grid3x3 lucide-grid-3x3"
aria-hidden="true"
data-testid="grid-icon"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
key="afitv7"
>
</rect>
<path d="M3 9h18"
key="1pudct"
>
</path>
<path d="M3 15h18"
key="5xshup"
>
</path>
<path d="M9 3v18"
key="fh3hqa"
>
</path>
<path d="M15 3v18"
key="14nvp0"
>
</path>
</svg>
`;
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 24 24"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
width="48"
height="48"
stroke="red"
stroke-width="1"
class="lucide lucide-icon lucide-grid3x3 lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3 lucide-icon"
aria-hidden="true"
data-testid="grid-icon"
>
@@ -84,15 +43,15 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
exports[`Using lucide icon components > should render a component 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 24 24"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
width="24"
height="24"
viewbox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
class="lucide lucide-icon lucide-grid3x3 lucide-grid-3x3"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3 lucide-icon"
aria-hidden="true"
>
<rect width="18"
@@ -121,3 +80,44 @@ exports[`Using lucide icon components > should render a component 1`] = `
</path>
</svg>
`;
exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewbox="0 0 24 24"
fill="none"
stroke="red"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3 lucide-icon"
aria-hidden="true"
data-testid="grid-icon"
>
<rect width="18"
height="18"
x="3"
y="3"
rx="2"
key="afitv7"
>
</rect>
<path d="M3 9h18"
key="1pudct"
>
</path>
<path d="M3 15h18"
key="5xshup"
>
</path>
<path d="M9 3v18"
key="fh3hqa"
>
</path>
<path d="M15 3v18"
key="14nvp0"
>
</path>
</svg>
`;
+1 -1
View File
@@ -96,7 +96,7 @@ describe('Using LucideProvider', () => {
expect(IconComponent).toHaveAttribute(
'class',
'lucide lucide-icon provider-class lucide-house icon-class',
'lucide lucide-house lucide-home lucide-icon provider-class icon-class',
);
});
});
@@ -57,7 +57,7 @@ describe('Using lucide icon components', () => {
expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML);
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
it('should scale the strokeWidth when absoluteStrokeWidth is set', () => {
const testId = 'grid-icon';
const { container, getByTestId } = render(() => (
<Grid
+5
View File
@@ -4,6 +4,11 @@
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@lucide/shared": ["../shared/src/index.ts"],
"@lucide/shared/types": ["../shared/src/build/types.ts"],
},
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
+7
View File
@@ -12,6 +12,13 @@
"test": "vitest run",
"test:watch": "vitest watch"
},
"exports": {
".": "./src/index.ts",
"./types": {
"types": "./src/build/types.ts",
"default": "./src/build/types.ts"
}
},
"devDependencies": {
"vite": "^7.3.6"
}
@@ -0,0 +1,30 @@
import type { LucideBuildParams, LucideIconData } from './types';
import buildLucideSvg from './buildLucideSvg';
/**
* Creates a base64 encoded data URI from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideDataUri(icon: LucideIconData, params: LucideBuildParams = {}) {
const svg = buildLucideSvg(icon, params);
// Browser
if (typeof btoa === 'function') {
// Ensure proper UTF-8 handling before base64
const utf8Bytes = new TextEncoder().encode(svg);
let binary = '';
for (const element of utf8Bytes) binary += String.fromCodePoint(element);
return `data:image/svg+xml;base64,${btoa(binary)}`;
}
// Node.js (and other JS runtimes with Buffer)
if (typeof Buffer !== 'undefined') {
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`;
}
throw new Error('No base64 encoder available in this environment.');
}
export default buildLucideDataUri;
@@ -0,0 +1,36 @@
import buildLucideIconNode from './buildLucideIconNode';
import type { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
const buildDomElement = (
document: Document,
[tagName, attributes, children = []]: LucideIconNode,
): Element => {
const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
for (const [attrName, value] of Object.entries(attributes)) {
if (value !== undefined && value !== null) {
element.setAttribute(attrName, String(value));
}
}
for (const node of children) {
const childNode = buildDomElement(document, node);
element.appendChild(childNode);
}
return element;
};
/**
* Creates an SvgElement from a Lucide icon object.
*
* @param document The document to create the Element in.
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideIconElement(
document: Document,
icon: LucideIconData,
params: LucideBuildParams = {},
) {
return buildDomElement(document, buildLucideIconNode(icon, params));
}
export default buildLucideIconElement;
@@ -0,0 +1,27 @@
import buildLucideIconNode from './buildLucideIconNode';
import type { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
/**
* Creates a React-compatible Lucide icon node (an svgson-like format) from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideIconForReact(
icon: LucideIconData,
params: LucideBuildParams = {},
): LucideIconNode {
return buildLucideIconNode(icon, {
...params,
attributeNames: {
...params.attributeNames,
class: 'className',
'stroke-width': 'strokeWidth',
'stroke-linecap': 'strokeLinecap',
'stroke-linejoin': 'strokeLinejoin',
'vector-effect': 'vectorEffect',
},
});
}
export default buildLucideIconForReact;
@@ -0,0 +1,69 @@
import type { LucideBuildParams, LucideIconData, LucideIconNode, SVGProps } from './types';
import defaultAttributes from './defaultAttributes';
import { mergeClasses } from '../utils/mergeClasses';
/**
* Creates a Lucide icon node (an svgson-like format) from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {}): LucideIconNode {
const attributeNames = params.attributeNames ?? {};
const getAttributeName = (attributeName: string) =>
attributeNames[attributeName] ?? attributeName;
const viewBoxWidth = icon.size ?? icon.width ?? defaultAttributes['width'];
const viewBoxHeight = icon.size ?? icon.height ?? defaultAttributes['height'];
const aliasClassNames =
icon.aliases
?.filter((alias: string): alias is string => typeof alias === 'string' && alias.trim() !== '')
.map((alias: string) => `lucide-${alias}`) ?? [];
const iconClassNames = [...(icon.name ? [`lucide-${icon.name}`] : []), ...aliasClassNames];
const classNamesFromClassName = params.className?.split(' ').filter(Boolean) ?? [];
const className =
params.includeDefaultClasses === false
? mergeClasses(...classNamesFromClassName)
: mergeClasses('lucide', ...iconClassNames, ...classNamesFromClassName);
const calculatedStrokeWidth = params.absoluteStrokeWidth
? (Number(params.strokeWidth ?? defaultAttributes['stroke-width']) *
Number(icon.size ?? icon.width)) /
Number(params.size ?? params.width ?? defaultAttributes['width'])
: params.strokeWidth ?? defaultAttributes['stroke-width'];
const attributes = {
...Object.entries(defaultAttributes).reduce((attrs, [attrName, value]) => {
attrs[getAttributeName(attrName)] = value;
return attrs;
}, {} as SVGProps),
...('color' in params && params.color && { [getAttributeName('stroke')]: params.color }),
...('size' in params &&
params.size && {
[getAttributeName('width')]: params.size,
[getAttributeName('height')]: params.size,
}),
...('width' in params && params.width && { [getAttributeName('width')]: params.width }),
...('height' in params && params.height && { [getAttributeName('height')]: params.height }),
[getAttributeName('stroke-width')]: calculatedStrokeWidth,
...(className && { [getAttributeName('class')]: className }),
[getAttributeName('viewBox')]: `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
...(params.hasA11yProp === false ? { [getAttributeName('aria-hidden')]: 'true' } : {}),
...('attributes' in params && params.attributes),
};
return [
'svg',
attributes,
icon.node.map((child): LucideIconNode => {
const [name, attrs, children] = child;
const nextAttrs = params.nonScalingStroke
? { [getAttributeName('vector-effect')]: 'non-scaling-stroke', ...attrs }
: attrs;
return children ? [name, nextAttrs, children] : [name, nextAttrs];
}),
];
}
export default buildLucideIconNode;
@@ -0,0 +1,20 @@
import buildLucideIconNode from './buildLucideIconNode';
import type { LucideBuildParams, LucideIconData, LucideIconNode } from './types';
const buildDomNode = ([tagName, attributes, children = []]: LucideIconNode): string =>
`<${tagName} ${Object.entries(attributes)
.filter(([, value]) => value !== undefined && value !== null)
.map(([attrName, value]) => `${attrName}="${String(value)}"`)
.join(' ')}>${children?.map((child) => buildDomNode(child)).join('')}</${tagName}>`;
/**
* Creates an SVG string from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideSvg(icon: LucideIconData, params: LucideBuildParams = {}) {
return buildDomNode(buildLucideIconNode(icon, params));
}
export default buildLucideSvg;
@@ -1,6 +1,4 @@
import type { Attrs } from './types.js';
const defaultAttributes: Attrs = {
const defaultAttributes = {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
@@ -10,6 +8,6 @@ const defaultAttributes: Attrs = {
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
};
} as const;
export default defaultAttributes;
@@ -0,0 +1,13 @@
const defaultReactAttributes = {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
} as const;
export default defaultReactAttributes;
+76
View File
@@ -0,0 +1,76 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type SVGProps = Record<string, any>;
/**
* A Lucide icon node (an svgson-like internal format)
*/
export type LucideIconNode<
TName extends string = string,
TProps extends Record<string, unknown> = SVGProps,
> =
| [name: TName, attributes: TProps]
| [name: TName, attributes: TProps, children: LucideIconNode<TName, TProps>[]];
/**
* A Lucide icon object that fully describes an icon to be displayed.
*/
export type LucideIconData<
TName extends string = string,
TProps extends Record<string, unknown> = SVGProps,
> = {
name?: string;
node: LucideIconNode<TName, TProps>[];
aliases?: string[];
} & (
| { size?: number; width?: never; height?: never }
| { size?: never; width?: number; height?: number }
);
/**
* Build parameters for creating a Lucide icon instance for display.
*/
export type LucideBuildParams<TProps extends Record<string, unknown> = SVGProps> = {
/**
* The color of the icon.
*/
color?: string;
/**
* The stroke width.
*/
strokeWidth?: string | number;
/**
* Whether to use absolute stroke width.
* @deprecated Use `nonScalingStroke` instead.
*/
absoluteStrokeWidth?: boolean;
/**
* Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements.
*/
nonScalingStroke?: boolean;
/**
* Extra CSS class names to pass to the SVG element.
*/
className?: string;
/**
* Include default Lucide classes on the SVG element.
*
* @default true
*/
includeDefaultClasses?: boolean;
/**
* Whether the rendered icon already has accessibility props.
* If false, `aria-hidden="true"` is added automatically.
*/
hasA11yProp?: boolean;
/**
* Any extra attributes to pass to the SVG element.
*/
attributes?: TProps;
/**
* The attribute names to use for various HTML attributes.
*/
attributeNames?: Record<string, string>;
} & (
| { size?: string | number | null; width?: never; height?: never }
| { size?: never; width?: string | number | null; height?: string | number | null }
);
+9
View File
@@ -1,7 +1,16 @@
export { default as buildLucideDataUri } from './build/buildLucideDataUri';
export { default as buildLucideIconElement } from './build/buildLucideIconElement';
export { default as buildLucideIconNode } from './build/buildLucideIconNode';
export { default as buildLucideIconForReact } from './build/buildLucideIconForReact';
export { default as buildLucideSvg } from './build/buildLucideSvg';
export { default as defaultAttributes } from './build/defaultAttributes';
export { default as defaultReactAttributes } from './build/defaultReactAttributes';
export type { LucideBuildParams, LucideIconData, LucideIconNode, SVGProps } from './build/types';
export * from './utils/hasA11yProp';
export * from './utils/isEmptyString';
export * from './utils/mergeClasses';
export * from './utils/toCamelCase';
export * from './utils/toKebabCase';
export * from './utils/toLucideIconData';
export * from './utils/toPascalCase';
export * from './utility-types';
+21 -4
View File
@@ -4,7 +4,24 @@
* @param {string} string
* @returns {string} A camelized string
*/
export const toCamelCase = <T extends string>(string: T) =>
string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) =>
p2 ? p2.toUpperCase() : p1.toLowerCase(),
);
export const toCamelCase = <T extends string>(string: T) => {
let out = '';
let upperNext = false;
for (const ch of string) {
if (ch === '-' || ch === '_' || ch <= ' ') {
upperNext = out.length > 0;
continue;
}
if (out.length === 0) {
out += ch.toLowerCase();
} else {
out += upperNext ? ch.toUpperCase() : ch;
}
upperNext = false;
}
return out as string;
};
@@ -0,0 +1,22 @@
import { toKebabCase } from './toKebabCase';
import type { LucideIconData, LucideIconNode } from '../build/types';
/**
* Converts legacy `(name, iconNode, aliases?)` icon arguments into icon data object format.
*/
export function toLucideIconData(
iconName: string,
iconNode: LucideIconNode[],
aliases: string[] = [],
): LucideIconData {
if (iconNode == null) {
throw new Error('[lucide]: iconNode is required when icon name is used');
}
return {
name: toKebabCase(iconName),
size: 24,
node: iconNode,
...(aliases.length > 0 ? { aliases } : {}),
};
}
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import { toLucideIconData } from '../src/utils/toLucideIconData';
describe('toLucideIconData', () => {
it('converts legacy name and node to icon data object', () => {
const iconNode = [['path', { d: 'M0 0h1' }]];
expect(toLucideIconData('AirVent', iconNode)).toEqual({
name: 'air-vent',
size: 24,
node: iconNode,
});
});
it('adds aliases when provided for legacy input', () => {
const iconNode = [['path', { d: 'M0 0h1' }]];
expect(toLucideIconData('AirVent', iconNode, ['vent'])).toEqual({
name: 'air-vent',
size: 24,
node: iconNode,
aliases: ['vent'],
});
});
it('throws when icon node is missing for legacy string input', () => {
expect(() => toLucideIconData('AirVent', undefined as never)).toThrowError(
'[lucide]: iconNode is required when icon name is used',
);
});
});
+1 -1
View File
@@ -48,7 +48,7 @@
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm copy:utils && pnpm build:icons && pnpm build:package && pnpm build:license",
"copy:license": "cp ../../LICENSE ./LICENSE",
"copy:utils": "mkdir -p ./src/utils && cp -rf ../../packages/shared/src/utils/hasA11yProp.ts ./src/utils/",
"copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/{hasA11yProp.ts,mergeClasses.ts} ./src/utils/ && node ./scripts/patchCopiedUtils.mts",
"clean": "rm -rf dist stats ./src/icons/*.{ts,svelte} ./src/aliases/{aliases,prefixed,suffixed}.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --exportFileName=index.ts --iconFileExtension=.svelte --importImportFileExtension=.svelte --separateIconFileExport --separateIconFileExportExtension=.ts --withAliases --aliasesFileExtension=.ts --separateAliasesFile --separateAliasesFileExtension=.ts --aliasImportFileExtension=.js --pretty=false",
"build:package": "svelte-package --input ./src",
+6 -5
View File
@@ -3,7 +3,7 @@ import { getHTMLBanner } from './license.mts';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(
async ({ iconName, children, getSvg, deprecated, deprecationReason }) => {
async ({ iconName, iconData, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
@@ -11,11 +11,12 @@ export default defineExportTemplate(
${getHTMLBanner()}
<script lang="ts">
import Icon from '../Icon.svelte';
import type { IconNode, IconProps } from '../types.js';
import type { LucideProps, LucideIconData } from '../types.js';
let props: IconProps = $props();
let props: LucideProps = $props();
const iconData: LucideIconData = ${JSON.stringify(iconData)};
const iconNode: IconNode = ${JSON.stringify(children)};
</script>
<!--
@@ -28,7 +29,7 @@ Lucide SVG icon component, renders SVG Element with children.
${deprecated ? `\n@deprecated ${deprecationReason}\n` : ''}\
-->
<Icon name="${iconName}" {...props} iconNode={iconNode} />
<Icon {...props} icon={iconData} />
`;
},
);

Some files were not shown because too many files have changed in this diff Show More