feat(packages): extract build logic to @lucide/shared, still wip

This commit is contained in:
Karsa
2026-05-22 21:59:12 +02:00
parent 071e4d2738
commit 2189ae0f55
70 changed files with 1125 additions and 596 deletions
+2
View File
@@ -23,6 +23,8 @@ 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/**/src/dynamicIconImports.ts
packages/**/DynamicIcon.d.ts
packages/**/dynamicIconImports.js
+4 -3
View File
@@ -17,14 +17,15 @@
"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": "mkdir -p ./src/utils && cp -f ../../packages/shared/src/build/buildLucideIconNode.ts ./src/utils/ && cp -f ../../packages/shared/src/build/types.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",
"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",
"version": "pnpm version --git-tag-version=false"
+16 -6
View File
@@ -12,6 +12,7 @@ import { LUCIDE_CONFIG } from './lucide-config';
import { LucideIconData, Nullable } from './types';
import defaultAttributes from './default-attributes';
import { lucideIconTemplate } from './lucide-icon-template';
import buildLucideIconNode from './utils/buildLucideIconNode';
/**
* @internal
@@ -82,18 +83,27 @@ export abstract class LucideIconBase {
const icon = this.icon();
if (icon) {
const absoluteStrokeWidth = this.absoluteStrokeWidth();
const { name, node, aliases = [] } = icon;
const classes = [name, ...aliases].map((item) => `lucide-${item}`);
const [, iconAttributes, iconNode] = buildLucideIconNode(icon as any, {
absoluteStrokeWidth,
hasA11yProp: this.title() != null,
});
const classes =
typeof iconAttributes['class'] === 'string'
? iconAttributes['class']
.split(' ')
.filter((cssClass) => cssClass && cssClass !== 'lucide')
: [];
for (const cssClass of classes) {
this.renderer.addClass(this.elRef.nativeElement, cssClass);
}
const contentRef = this.contentRef();
const refChild = contentRef.nativeElement;
const elements = node.map(([name, attrs]) => {
const elements = (iconNode ?? []).map(([name, attrs]) => {
const element = this.renderer.createElement(name, 'http://www.w3.org/2000/svg');
if (absoluteStrokeWidth) {
this.renderer.setAttribute(element, 'vector-effect', 'non-scaling-stroke');
}
Object.entries(attrs).forEach(([name, value]) =>
this.renderer.setAttribute(
element,
+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};
`;
});
},
);
+31 -13
View File
@@ -1,32 +1,50 @@
---
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,
'stroke-width': strokeWidth = 2,
absoluteStrokeWidth = false,
name,
aliases,
iconNode = [],
class: className,
...rest
} = Astro.props;
const calculatedStrokeWidth =
absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth;
const [, rawIconAttributes, builtIconNode = []] = buildLucideIconNode(
{
name,
aliases,
size: 24,
node: iconNode,
},
{
color,
size,
strokeWidth: calculatedStrokeWidth,
className,
hasA11yProp: hasA11yProp(rest),
},
);
const { class: iconClassName, ...builtIconAttributes } = rawIconAttributes;
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]}
{...iconAttributes}
class={iconClassName}
>
{iconNode.map(([Tag, attrs]) => <Tag {...attrs} />)}
{builtIconNode.map(([Tag, attrs]) => <Tag {...attrs} />)}
<slot />
</svg>
+6 -9
View File
@@ -1,11 +1,9 @@
import type { AstroComponentFactory } from 'astro/runtime/server/render/astro/factory.js';
import type { IconNode } from './types';
import type { LucideIconData } 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 => {
export default (iconData: LucideIconData): AstroComponentFactory => {
const Component = createComponent(
($$result, $$props: Record<string, any>, $$slots) => {
const { class: className, ...restProps } = $$props;
@@ -14,11 +12,10 @@ export default (iconName: string, iconNode: IconNode): AstroComponentFactory =>
'Icon',
Icon,
{
class: mergeClasses(
Boolean(iconName) && `lucide-${toKebabCase(iconName)}`,
Boolean(className) && className,
),
iconNode,
class: className,
name: iconData.name,
aliases: iconData.aliases,
iconNode: iconData.node,
...restProps,
},
{ default: () => render`${renderSlot($$result, $$slots['default'])}` },
+8
View File
@@ -9,6 +9,8 @@ export interface IconProps extends SVGAttributes {
'stroke-width'?: number | string;
absoluteStrokeWidth?: boolean;
class?: string;
name?: string;
aliases?: string[];
iconNode?: IconNode;
title?: string;
}
@@ -17,6 +19,12 @@ export type SVGAttributes = HTMLAttributes<'svg'>;
export type IconNode = IconNodeChild[];
export interface LucideIconData {
name: string;
node: IconNode;
aliases?: string[];
}
type IconNodeChild = [elementName: SVGElements, attrs: SVGAttributes];
// All possible svg elements according to the Astro definitions
@@ -47,7 +47,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
>
<rect width="18"
height="18"
@@ -96,7 +96,7 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
>
<rect width="18"
height="18"
@@ -165,7 +165,7 @@ exports[`Using lucide icon components > should render a component 1`] = `
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
>
<rect width="18"
height="18"
@@ -196,7 +196,7 @@ exports[`Using lucide icon components > should render the icon with default attr
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3"
>
<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();
+1
View File
@@ -64,6 +64,7 @@
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "^1.0.0",
"@lucide/rollup-plugins": "workspace:*",
"@lucide/shared": "workspace:*",
"@rollup/plugin-replace": "^6.0.2",
"@testing-library/jest-dom": "^6.6.3",
"jest-serializer-html": "^7.1.0",
+2 -18
View File
@@ -1,5 +1,5 @@
import { buildLucideDataUri as sharedBuildLucideDataUri } from '@lucide/shared';
import { LucideBuildParams, LucideIconData } from './types';
import buildLucideSvg from './buildLucideSvg';
/**
* Creates a base64 encoded data URI from a Lucide icon object.
@@ -8,23 +8,7 @@ import buildLucideSvg from './buildLucideSvg';
* @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.');
return sharedBuildLucideDataUri(icon as any, params as any);
}
export default buildLucideDataUri;
+3 -18
View File
@@ -1,20 +1,5 @@
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;
};
import { buildLucideIconElement as sharedBuildLucideIconElement } from '@lucide/shared';
import { LucideBuildParams, LucideIconData } from './types';
/**
* Creates an SvgElement from a Lucide icon object.
@@ -28,7 +13,7 @@ function buildLucideIconElement(
icon: LucideIconData,
params: LucideBuildParams = {},
) {
return buildDomElement(document, buildLucideIconNode(icon, params));
return sharedBuildLucideIconElement(document, icon as any, params as any);
}
export default buildLucideIconElement;
+2 -43
View File
@@ -1,17 +1,6 @@
import { buildLucideIconNode as sharedBuildLucideIconNode } from '@lucide/shared';
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.
*
@@ -19,37 +8,7 @@ const defaultAttributes = {
* @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,
]),
];
return sharedBuildLucideIconNode(icon as any, params as any) as LucideIconNode;
}
export default buildLucideIconNode;
+3 -8
View File
@@ -1,10 +1,5 @@
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}>`;
import { buildLucideSvg as sharedBuildLucideSvg } from '@lucide/shared';
import { LucideBuildParams, LucideIconData } from './types';
/**
* Creates an SVG string from a Lucide icon object.
@@ -13,7 +8,7 @@ const buildDomNode = ([tagName, attributes, children = []]: LucideIconNode): str
* @param params Additional build parameters.
*/
function buildLucideSvg(icon: LucideIconData, params: LucideBuildParams = {}) {
return buildDomNode(buildLucideIconNode(icon, params));
return sharedBuildLucideSvg(icon as any, params as any);
}
export default buildLucideSvg;
+1 -43
View File
@@ -1,43 +1 @@
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 });
export type { LucideBuildParams, LucideIconData, LucideIconNode, SVGProps } from '@lucide/shared';
@@ -89,4 +89,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();
});
});
@@ -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};
`;
});
},
);
+28 -17
View File
@@ -1,12 +1,13 @@
import { h, toChildArray } from 'preact';
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
import defaultAttributes from './defaultAttributes';
import type { IconNode, LucideProps } from './types';
import type { LucideIconNode, LucideProps } from './types';
import { useLucideContext } from './context';
import { mergeClasses } from '@lucide/shared';
import { hasA11yProp } from '@lucide/shared';
interface IconComponentProps extends LucideProps {
iconNode: IconNode;
name?: string;
aliases?: string[];
iconNode: LucideIconNode;
}
/**
@@ -19,8 +20,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.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
*/
@@ -31,6 +32,8 @@ const Icon = ({
absoluteStrokeWidth,
children,
iconNode,
name,
aliases,
class: classes = '',
...rest
}: IconComponentProps) => {
@@ -47,20 +50,28 @@ const Icon = ({
? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize)
: strokeWidth ?? contextStrokeWidth;
return h(
'svg',
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(
{
...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,
name,
aliases,
size: 24,
node: iconNode,
},
{
color: color ?? contextColor,
size: size ?? contextSize ?? defaultAttributes.width,
strokeWidth: calculatedStrokeWidth,
className: mergeClasses(contextClass, classes as string),
hasA11yProp: hasAccessibleProp,
},
[...iconNode.map(([tag, attrs]) => h(tag, attrs)), ...toChildArray(children)],
);
return h('svg', { ...svgAttributes, ...rest }, [
...builtIconNode.map(([tag, attrs]) => h(tag, attrs)),
...toChildArray(children),
]);
};
export default Icon;
+38 -16
View File
@@ -1,34 +1,56 @@
import { h, type JSX } from 'preact';
import { mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared';
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 iconData Icon data object.
* @returns Lucide icon component.
*/
const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
function createLucideIcon(iconData: 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,
),
name: iconData.name,
aliases: iconData.aliases,
iconNode: iconData.node,
class: mergeClasses<string | JSX.SignalLike<string | undefined>>(classes, className),
},
children,
);
Component.displayName = toPascalCase(iconName);
Component.displayName = toPascalCase(iconData.name);
return Component;
};
}
export default createLucideIcon;
+15 -1
View File
@@ -1,6 +1,20 @@
import { type FunctionComponent, type JSX } from 'preact';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared';
export type IconNode = [elementName: keyof JSX.IntrinsicElements, attrs: Record<string, string>][];
export type LucideIconNode = SharedLucideIconNode[];
export type LucideIconData = SharedLucideIconData & {
name: string;
node: LucideIconNode;
};
/**
* @deprecated Use LucideIconNode instead.
*/
export type IconNode = LucideIconNode;
export interface LucideProps extends Partial<Omit<JSX.SVGAttributes, 'ref' | 'size'>> {
color?: string;
@@ -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"
@@ -41,7 +41,7 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
stroke-width="1"
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"
@@ -72,7 +72,7 @@ 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"
@@ -83,6 +83,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,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};
`;
});
},
);
+50 -11
View File
@@ -1,15 +1,39 @@
import { createElement, forwardRef, type FunctionComponent } from 'react';
import * as NativeSvg from 'react-native-svg';
import { buildLucideIconNode } from '@lucide/shared';
import defaultAttributes, { childDefaultAttributes } from './defaultAttributes';
import { IconNode, LucideProps } from './types';
import { LucideIconNode, LucideProps } from './types';
import { useLucideContext } from './context';
interface IconComponentProps extends LucideProps {
iconNode: IconNode;
iconNode: LucideIconNode;
testID?: string;
className?: string;
}
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 +44,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
*/
@@ -52,6 +76,20 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize)
: strokeWidth ?? contextStrokeWidth;
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(
{
size: 24,
node: iconNode,
},
{
color: color ?? contextColor ?? defaultAttributes.stroke,
size: size ?? contextSize ?? defaultAttributes.width,
strokeWidth: calculatedStrokeWidth,
className,
includeDefaultClasses: false,
},
);
const customAttrs = {
stroke: color ?? contextColor ?? defaultAttributes.stroke,
strokeWidth: calculatedStrokeWidth,
@@ -62,21 +100,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]) || []),
@@ -1,26 +1,51 @@
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 iconData Icon data object.
* @returns Lucide icon component.
*/
const createLucideIcon = (iconName: string, iconNode: IconNode) => {
function createLucideIcon(iconData: 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,
iconNode: iconData.node,
...props,
}),
);
Component.displayName = toPascalCase(iconName);
Component.displayName = toPascalCase(iconData.name);
return Component;
};
}
export default createLucideIcon;
+13 -14
View File
@@ -1,22 +1,21 @@
import type { ForwardRefExoticComponent } from 'react';
import type { SvgProps } from 'react-native-svg';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared';
export type LucideIconNode = SharedLucideIconNode[];
export type LucideIconData = SharedLucideIconData & {
name: string;
node: LucideIconNode;
};
/**
* 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;
@@ -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 createLucideIcon from '../createLucideIcon';
import { IconNode } from '../types';
import type { LucideIconData } from '../types';
export const __iconNode: IconNode = ${JSON.stringify(children)}
export const __iconData: LucideIconData = ${JSON.stringify(iconData)}
/**
* @component @name ${componentName}
@@ -29,8 +23,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};
`;
});
},
);
+18 -6
View File
@@ -1,11 +1,15 @@
'use client';
import { createElement, forwardRef, useEffect, useState } from 'react';
import { IconNode, LucideIcon, LucideProps } from './types';
import { LucideIcon, LucideIconData, LucideIconNode, LucideProps } from './types';
import dynamicIconImports from './dynamicIconImports';
import Icon from './Icon';
export type DynamicIconModule = { default: LucideIcon; __iconNode: IconNode };
export type DynamicIconModule = {
default: LucideIcon;
__iconNode?: LucideIconNode;
__iconData?: LucideIconData;
};
export type IconName = keyof typeof dynamicIconImports;
@@ -24,7 +28,15 @@ 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.node;
}
if (icon.__iconNode != null) {
return icon.__iconNode;
}
throw new Error('[lucide-react]: Failed to resolve icon data for DynamicIcon');
}
/**
@@ -37,14 +49,14 @@ async function getIconNode(name: IconName) {
* @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
*/
const DynamicIcon = forwardRef<SVGSVGElement, DynamicIconComponentProps>(
({ name, fallback: Fallback, ...props }, ref) => {
const [iconNode, setIconNode] = useState<IconNode>();
const [iconNode, setIconNode] = useState<LucideIconNode>();
useEffect(() => {
getIconNode(name)
+62 -14
View File
@@ -1,15 +1,40 @@
'use client';
import { createElement, forwardRef } from 'react';
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
import defaultAttributes from './defaultAttributes';
import { IconNode, LucideProps } from './types';
import { mergeClasses, hasA11yProp } from '@lucide/shared';
import { LucideIconNode, LucideProps } from './types';
import { useLucideContext } from './context';
interface IconComponentProps extends LucideProps {
iconNode: IconNode;
name?: string;
aliases?: string[];
iconNode: LucideIconNode;
}
const toReactAttributeName = (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 toReactAttributes = (attributes: Record<string, any>) => {
return Object.entries(attributes).reduce<Record<string, any>>((acc, [attributeName, value]) => {
if (value === undefined) {
return acc;
}
acc[toReactAttributeName(attributeName)] = value;
return acc;
}, {});
};
/**
* Lucide icon component
*
@@ -20,14 +45,25 @@ 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
*/
const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
(
{ color, size, strokeWidth, absoluteStrokeWidth, className = '', children, iconNode, ...rest },
{
color,
size,
strokeWidth,
absoluteStrokeWidth,
className = '',
children,
iconNode,
name,
aliases,
...rest
},
ref,
) => {
const {
@@ -43,21 +79,33 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
? (Number(strokeWidth ?? contextStrokeWidth) * 24) / Number(size ?? contextSize)
: strokeWidth ?? contextStrokeWidth;
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(
{
name,
aliases,
size: 24,
node: iconNode,
},
{
color: color ?? contextColor,
size: size ?? contextSize ?? defaultAttributes.width,
strokeWidth: calculatedStrokeWidth,
className: mergeClasses(contextClass, className),
hasA11yProp: hasAccessibleProp,
},
);
return createElement(
'svg',
{
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' }),
...toReactAttributes(svgAttributes),
...rest,
},
[
...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
...builtIconNode.map(([tag, attrs]) => createElement(tag, attrs)),
...(Array.isArray(children) ? children : [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 iconData Icon data object.
* @returns Lucide icon component.
*/
const createLucideIcon = (iconName: string, iconNode: IconNode) => {
function createLucideIcon(iconData: 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>(({ className, ...props }, ref) =>
createElement(Icon, {
ref,
iconNode,
className: mergeClasses(
`lucide-${toKebabCase(toPascalCase(iconName))}`,
`lucide-${iconName}`,
className,
),
name: iconData.name,
aliases: iconData.aliases,
iconNode: iconData.node,
className,
...props,
}),
);
Component.displayName = toPascalCase(iconName);
Component.displayName = toPascalCase(iconData.name);
return Component;
};
}
export default createLucideIcon;
+13 -14
View File
@@ -1,21 +1,20 @@
import type { SVGProps, ForwardRefExoticComponent, RefAttributes } from 'react';
import type {
LucideIconData as SharedLucideIconData,
LucideIconNode as SharedLucideIconNode,
} from '@lucide/shared';
export type LucideIconNode = SharedLucideIconNode[];
export type LucideIconData = SharedLucideIconData & {
name: string;
node: LucideIconNode;
};
/**
* 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 type SVGAttributes = Partial<SVGProps<SVGSVGElement>>;
type ElementAttributes = RefAttributes<SVGSVGElement> & SVGAttributes;
@@ -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,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"
@@ -41,7 +41,7 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
stroke-width="1"
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"
@@ -72,7 +72,7 @@ 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"
+4 -1
View File
@@ -94,6 +94,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,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} iconNode={iconData.node} name={iconData.name} aliases={iconData.aliases} />
)
export default ${componentName};
`;
});
},
);
+49 -37
View File
@@ -1,12 +1,13 @@
import { For, splitProps, useContext } from 'solid-js';
import { Dynamic } from 'solid-js/web';
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
import defaultAttributes from './defaultAttributes';
import { IconNode, LucideProps } from './types';
import { LucideContext } from './context';
import { hasA11yProp, mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared';
interface IconProps {
name?: string;
aliases?: string[];
iconNode: IconNode;
}
@@ -18,49 +19,60 @@ const Icon = (props: LucideProps & IconProps) => {
'children',
'class',
'name',
'aliases',
'iconNode',
'absoluteStrokeWidth',
]);
const globalProps = useContext(LucideContext);
const calculatedStrokeWidth =
(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'],
);
const hasAccessibleProp = Boolean(localProps.children) || hasA11yProp(rest);
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(
{
name: localProps.name,
aliases: localProps.aliases,
size: 24,
node: localProps.iconNode,
},
{
color: localProps.color ?? globalProps.color ?? defaultAttributes.stroke,
size: localProps.size ?? globalProps.size ?? defaultAttributes.width,
strokeWidth: calculatedStrokeWidth,
className: mergeClasses('lucide-icon', globalProps.class, localProps.class),
hasA11yProp: hasAccessibleProp,
},
);
const svgElementAttributes = {
xmlns: svgAttributes.xmlns,
viewBox: svgAttributes.viewBox,
fill: svgAttributes.fill,
'stroke-linecap': svgAttributes['stroke-linecap'],
'stroke-linejoin': svgAttributes['stroke-linejoin'],
width: svgAttributes.width,
height: svgAttributes.height,
stroke: svgAttributes.stroke,
'stroke-width': svgAttributes['stroke-width'],
class: svgAttributes.class,
'aria-hidden': svgAttributes['aria-hidden'],
...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 {...svgElementAttributes}>
<For each={builtIconNode}>
{([elementName, attrs]) => {
return (
<Dynamic
+5
View File
@@ -1,6 +1,11 @@
import { JSX } from 'solid-js/jsx-runtime';
export type IconNode = [elementName: keyof JSX.IntrinsicElements, attrs: Record<string, string>][];
export interface LucideIconData {
name: string;
node: IconNode;
aliases?: string[];
}
export type SVGAttributes = Partial<JSX.SvgSVGAttributes<SVGSVGElement>>;
export interface LucideProps extends SVGAttributes {
@@ -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"
@@ -10,7 +10,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
height="48"
stroke="red"
stroke-width="4"
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"
>
@@ -51,7 +51,7 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
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"
>
@@ -92,7 +92,7 @@ exports[`Using lucide icon components > should render a component 1`] = `
height="24"
stroke="currentColor"
stroke-width="2"
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"
>
<rect width="18"
+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',
);
});
});
+12 -35
View File
@@ -1,33 +1,6 @@
import defaultAttributes from './defaultAttributes';
import { buildLucideIconElement } from '@lucide/shared';
import { IconNode, SVGProps } from './types';
type CreateSVGElementParams = [tag: string, attrs: SVGProps, children?: IconNode];
/**
* Creates a new SVGElement
* @param {string} tag - Tag name of the element
* @param {object} attrs - Attributes of the element
* @param {array} children - Children of the element
* @returns {SVGElement}
*/
const createSVGElement = ([tag, attrs, children]: CreateSVGElementParams) => {
const element = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.keys(attrs).forEach((name) => {
element.setAttribute(name, String(attrs[name]));
});
if (children?.length) {
children.forEach((child) => {
const childElement = createSVGElement(child);
element.appendChild(childElement);
});
}
return element;
};
/**
* Creates a new HTMLElement from icon node
* @param {array} iconNode - Icon node to be converted to an element
@@ -35,13 +8,17 @@ const createSVGElement = ([tag, attrs, children]: CreateSVGElementParams) => {
* @returns {HTMLElement}
*/
const createElement = (iconNode: IconNode, customAttrs: SVGProps = {}) => {
const tag = 'svg';
const attrs = {
...defaultAttributes,
...customAttrs,
};
return createSVGElement([tag, attrs, iconNode]);
return buildLucideIconElement(
document,
{
size: 24,
node: iconNode,
},
{
includeDefaultClasses: false,
attributes: customAttrs,
},
) as SVGElement;
};
export default createElement;
+6 -7
View File
@@ -1,7 +1,6 @@
import createElement from './createElement';
import defaultAttributes from './defaultAttributes';
import { Icons, SVGProps } from './types';
import { hasA11yProp, mergeClasses, toPascalCase } from '@lucide/shared';
import createElement from './createElement';
import { Icons, SVGProps } from './types';
export type CustomAttrs = { [attr: string]: any };
@@ -63,15 +62,15 @@ const replaceElement = (element: Element, { nameAttr, icons, attrs }: ReplaceEle
}
const elementAttrs = getAttrs(element);
const ariaProps = hasA11yProp(elementAttrs) ? {} : { 'aria-hidden': 'true' };
const { class: _attrsClassName, ...attrsWithoutClass } = attrs;
const { class: _elementClassName, ...elementAttrsWithoutClass } = elementAttrs;
const iconAttrs = {
...defaultAttributes,
'data-lucide': iconName,
...ariaProps,
...attrs,
...elementAttrs,
...attrsWithoutClass,
...elementAttrsWithoutClass,
} satisfies SVGProps;
const elementClassNames = getClassNames(elementAttrs);
@@ -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,89 @@
import type { LucideBuildParams, LucideIconData, LucideIconNode, SVGProps } from './types';
export type { LucideBuildParams, LucideIconData, LucideIconNode, SVGProps } from './types';
const defaultAttributes: SVGProps = {
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',
};
const mergeClasses = (...classes: Array<string | undefined>) =>
classes
.filter(
(className, index, array): className is string =>
typeof className === 'string' &&
className.trim() !== '' &&
array.indexOf(className) === index,
)
.join(' ')
.trim();
const toString = (value: string | number) =>
typeof value === 'number' ? value.toString(10) : value.toString();
/**
* 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 !== undefined
? icon.size
: 'width' in icon
? icon.width
: undefined) ?? defaultAttributes['width'];
const viewBoxHeight =
('size' in icon && icon.size !== undefined
? icon.size
: 'height' in icon
? icon.height
: undefined) ?? defaultAttributes['height'];
const aliasClassNames =
icon.aliases
?.filter((alias): alias is string => typeof alias === 'string' && alias.trim() !== '')
.map((alias) => `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 attributes = {
...defaultAttributes,
...('color' in params && params.color && { stroke: params.color }),
...('size' in params &&
params.size && {
width: toString(params.size),
height: toString(params.size),
}),
...('width' in params && params.width && { width: toString(params.width) }),
...('height' in params && params.height && { height: toString(params.height) }),
...('strokeWidth' in params &&
params.strokeWidth && { 'stroke-width': toString(params.strokeWidth) }),
...(className && { class: className }),
viewBox: `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
...(params.hasA11yProp === false ? { 'aria-hidden': 'true' } : {}),
...('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;
@@ -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;
+54
View File
@@ -0,0 +1,54 @@
export type SVGProps = Record<string, any>;
/**
* 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?: string | 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;
/**
* 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?: SVGProps;
} & ({ size?: string | number } | { width?: string | number; height?: string | number });
+6
View File
@@ -1,7 +1,13 @@
export { default as buildLucideDataUri } from './build/buildLucideDataUri';
export { default as buildLucideIconElement } from './build/buildLucideIconElement';
export { default as buildLucideIconNode } from './build/buildLucideIconNode';
export { default as buildLucideSvg } from './build/buildLucideSvg';
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';
@@ -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',
);
});
});
+6 -5
View File
@@ -3,19 +3,20 @@ 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, componentName, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `\
${getHTMLBanner()}
<script lang="ts">
${getHTMLBanner()}
import Icon from '../Icon.svelte';
import type { IconNode, IconProps } from '../types.js';
import type { IconProps, LucideIconData } from '../types.js';
let props: IconProps = $props();
const iconNode: IconNode = ${JSON.stringify(children)};
const iconData: LucideIconData = ${JSON.stringify(iconData)};
</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 name={iconData.name} aliases={iconData.aliases} {...props} iconNode={iconData.node} />
`;
},
);
+45 -12
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import defaultAttributes from './defaultAttributes.js';
import buildLucideIconNode from './utils/buildLucideIconNode.js';
import type { IconProps } from './types.js';
import { hasA11yProp } from './utils/hasA11yProp.js';
import { getLucideContext } from './context.js';
@@ -8,6 +8,7 @@
const {
name,
aliases,
color = globalProps.color ?? 'currentColor',
size = globalProps.size ?? 24,
strokeWidth = globalProps.strokeWidth ?? 2,
@@ -20,19 +21,51 @@
const calculatedStrokeWidth = $derived(
absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
);
const iconClassName = $derived(
['lucide-icon', globalProps.class, props.class]
.filter(
(className): className is string =>
typeof className === 'string' && className.trim() !== '',
)
.join(' '),
);
const hasAccessibleProp = $derived(
Boolean(children) || hasA11yProp(props as Record<string, any>),
);
const [, builtSvgAttributes, builtIconNode = []] = $derived(
buildLucideIconNode(
{
name,
aliases,
size: 24,
node: iconNode,
},
{
size,
color,
strokeWidth: calculatedStrokeWidth,
className: iconClassName,
hasA11yProp: hasAccessibleProp,
},
),
);
const iconAttributes = $derived({
...builtSvgAttributes,
...props,
width: builtSvgAttributes.width,
height: builtSvgAttributes.height,
stroke: builtSvgAttributes.stroke,
'stroke-width': builtSvgAttributes['stroke-width'],
class: builtSvgAttributes.class,
});
</script>
<svg
{...defaultAttributes}
{...!children && !hasA11yProp(props) && { 'aria-hidden': 'true' }}
{...props}
width={size}
height={size}
stroke={color}
stroke-width={calculatedStrokeWidth}
class={['lucide-icon lucide', globalProps.class, name && `lucide-${name}`, props.class]}
>
{#each iconNode as [tag, attrs]}
<svg {...iconAttributes}>
{#each builtIconNode as [tag, attrs]}
<svelte:element
this={tag as string}
{...attrs}
+7
View File
@@ -14,8 +14,15 @@ type IconNodeElements =
export type IconNode = [elementName: IconNodeElements, attrs: Attrs][];
export interface LucideIconData {
name: string;
node: IconNode;
aliases?: string[];
}
export interface LucideProps extends Attrs {
name?: string;
aliases?: string[];
color?: string;
size?: number | string;
strokeWidth?: number | string;
@@ -3,7 +3,7 @@
exports[`Using Icon Component > should render icon and match snapshot 1`] = `
<svg
aria-hidden="true"
class="lucide-icon lucide"
class="lucide lucide-icon"
fill="none"
height="48"
stroke="red"
@@ -3,7 +3,7 @@
exports[`Using lucide icon components > should add a class to the element 1`] = `
<svg
aria-hidden="true"
class="lucide-icon lucide lucide-smile my-icon"
class="lucide lucide-smile lucide-icon my-icon"
fill="none"
height="24"
stroke="currentColor"
@@ -59,7 +59,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
<div>
<svg
aria-hidden="true"
class="lucide-icon lucide lucide-smile"
class="lucide lucide-smile lucide-icon"
fill="none"
height="48"
stroke="red"
@@ -124,8 +124,8 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-smile lucide-icon"
aria-hidden="true"
class="lucide-icon lucide lucide-smile"
>
<circle cx="12"
cy="12"
@@ -153,7 +153,7 @@ exports[`Using lucide icon components > should render an component 1`] = `
<div>
<svg
aria-hidden="true"
class="lucide-icon lucide lucide-smile"
class="lucide lucide-smile lucide-icon"
fill="none"
height="24"
stroke="currentColor"
@@ -211,7 +211,7 @@ exports[`Using lucide icon components > should render an component 1`] = `
exports[`Using lucide icon components > should render an icon slot 1`] = `
<div>
<svg
class="lucide-icon lucide lucide-smile"
class="lucide lucide-smile lucide-icon"
fill="none"
height="24"
stroke="currentColor"
+2 -1
View File
@@ -1,5 +1,6 @@
import { expect } from 'vitest';
import '@testing-library/jest-dom/vitest';
import * as matchers from '@testing-library/jest-dom/matchers';
import htmlSerializer from 'jest-serializer-html';
expect.extend(matchers);
expect.addSnapshotSerializer(htmlSerializer);
+10 -15
View File
@@ -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 createLucideIcon from '../createLucideIcon';
import { IconNode } from '../types';
import type { LucideIconData } from '../types';
export const __iconNode: IconNode = ${JSON.stringify(children)}
export const __iconData: LucideIconData = ${JSON.stringify(iconData)}
/**
* @component @name ${componentName}
@@ -29,8 +23,9 @@ export const __iconNode: IconNode = ${JSON.stringify(children)}
* @returns {FunctionalComponent} Vue component
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName} = createLucideIcon('${iconName}', __iconNode);
const ${componentName} = createLucideIcon(__iconData);
export default ${componentName};
`;
});
},
);
+33 -17
View File
@@ -1,5 +1,5 @@
import { computed, type FunctionalComponent, h } from 'vue';
import { isEmptyString, mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared';
import { buildLucideIconNode, isEmptyString, toKebabCase } from '@lucide/shared';
import defaultAttributes from './defaultAttributes';
import { IconNode, LucideProps } from './types';
import { useLucideProps } from './context';
@@ -7,11 +7,13 @@ import { useLucideProps } from './context';
interface IconProps {
iconNode: IconNode;
name: string;
aliases?: string[];
}
const Icon: FunctionalComponent<LucideProps & IconProps> = (
{
name,
aliases,
iconNode,
absoluteStrokeWidth,
'absolute-stroke-width': absoluteStrokeWidthKebabCase,
@@ -54,25 +56,39 @@ const Icon: FunctionalComponent<LucideProps & IconProps> = (
return strokeWidthValue;
});
return h(
'svg',
const normalizedName = computed(() => (name != null ? toKebabCase(name) : undefined));
const iconName = computed(() => (normalizedName.value ? `${normalizedName.value}-icon` : 'icon'));
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(
{
...defaultAttributes,
...props,
width: size ?? contextSize ?? defaultAttributes.width,
height: size ?? contextSize ?? defaultAttributes.height,
stroke: color ?? contextColor ?? defaultAttributes.stroke,
'stroke-width': calculatedStrokeWidth.value,
class: mergeClasses(
'lucide',
contextClass,
...(name
? [`lucide-${toKebabCase(toPascalCase(name))}-icon`, `lucide-${toKebabCase(name)}`]
: ['lucide-icon']),
),
name: iconName.value,
aliases,
size: 24,
node: iconNode,
},
{
size: size ?? contextSize ?? defaultAttributes.width,
color: color ?? contextColor ?? defaultAttributes.stroke,
strokeWidth: calculatedStrokeWidth.value,
className: contextClass,
},
[...iconNode.map((child) => h(...child)), ...(slots.default ? [slots.default()] : [])],
);
const iconAttributes = {
...svgAttributes,
...props,
width: svgAttributes.width,
height: svgAttributes.height,
stroke: svgAttributes.stroke,
'stroke-width': svgAttributes['stroke-width'],
class: svgAttributes.class,
};
return h('svg', iconAttributes, [
...builtIconNode.map((child) => h(...child)),
...(slots.default ? [slots.default()] : []),
]);
};
export default Icon;
+6 -6
View File
@@ -1,23 +1,23 @@
import { h } from 'vue';
import { IconNode, LucideIcon } from './types';
import { LucideIcon, LucideIconData } from './types';
import Icon from './Icon';
/**
* Create a Lucide icon component
* @param {string} iconName
* @param {array} iconNode
* @param {object} iconData
* @returns {FunctionalComponent} LucideIcon
*/
const createLucideIcon =
(iconName: string, iconNode: IconNode): LucideIcon =>
(iconData: LucideIconData): LucideIcon =>
(props, { slots, attrs }) =>
h(
Icon,
{
...attrs,
...props,
iconNode,
name: iconName,
iconNode: iconData.node,
name: iconData.name,
aliases: iconData.aliases,
},
slots.default ? { default: slots.default } : undefined,
);
+5
View File
@@ -8,4 +8,9 @@ export interface LucideProps extends Partial<SVGAttributes> {
}
export type IconNode = [elementName: string, attrs: Record<string, string | number>][];
export interface LucideIconData {
name: string;
node: IconNode;
aliases?: string[];
}
export type LucideIcon = FunctionalComponent<LucideProps>;
@@ -2,7 +2,7 @@
exports[`Using Icon Component > should render icon and match snapshot 1`] = `
<svg
class="lucide lucide-air-vent-icon lucide-air-vent"
class="lucide lucide-air-vent-icon"
fill="none"
height="48"
stroke="red"
@@ -2,7 +2,7 @@
exports[`Using lucide icon context > should render the icon with LucideProvider 1`] = `
<svg
class="lucide provider-class lucide-house-icon lucide-house"
class="lucide lucide-house-icon lucide-home provider-class"
fill="none"
height="48"
stroke="red"
@@ -3,7 +3,7 @@
exports[`Using lucide icon components > should add a class to the element 1`] = `
<div>
<svg
class="lucide lucide-smile-icon lucide-smile my-icon"
class="lucide lucide-smile-icon my-icon"
fill="none"
height="24"
stroke="currentColor"
@@ -41,7 +41,7 @@ exports[`Using lucide icon components > should add a class to the element 1`] =
exports[`Using lucide icon components > should add a style attribute to the element 1`] = `
<div>
<svg
class="lucide lucide-smile-icon lucide-smile"
class="lucide lucide-smile-icon"
fill="none"
height="24"
stroke="currentColor"
@@ -80,7 +80,7 @@ exports[`Using lucide icon components > should add a style attribute to the elem
exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = `
<div>
<svg
class="lucide lucide-smile-icon lucide-smile"
class="lucide lucide-smile-icon"
fill="none"
height="48"
stroke="red"
@@ -118,7 +118,7 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
exports[`Using lucide icon components > should pass children to the icon slot 1`] = `
<div>
<svg
class="lucide lucide-smile-icon lucide-smile"
class="lucide lucide-smile-icon"
fill="none"
height="24"
stroke="currentColor"
@@ -161,7 +161,7 @@ exports[`Using lucide icon components > should pass children to the icon slot 1`
exports[`Using lucide icon components > should render an component 1`] = `
<div>
<svg
class="lucide lucide-smile-icon lucide-smile"
class="lucide lucide-smile-icon"
fill="none"
height="24"
stroke="currentColor"
+1 -1
View File
@@ -59,7 +59,7 @@ describe('Using lucide icon context', () => {
expect(IconComponent).toHaveAttribute(
'class',
'lucide provider-class lucide-house-icon lucide-house icon-class',
'lucide lucide-house-icon lucide-home provider-class icon-class',
);
});
});
+3
View File
@@ -357,6 +357,9 @@ importers:
'@lucide/rollup-plugins':
specifier: workspace:*
version: link:../../tools/rollup-plugins
'@lucide/shared':
specifier: workspace:*
version: link:../shared
'@rollup/plugin-replace':
specifier: ^6.0.2
version: 6.0.3(rollup@4.59.0)