mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-12 13:38:32 +00:00
feat(packages): extract build logic to @lucide/shared, still wip
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'])}` },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 +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};
|
||||
`;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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} />
|
||||
`;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+3
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user