diff --git a/.gitignore b/.gitignore index 691b0bc39..7aab235ad 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/packages/angular/package.json b/packages/angular/package.json index 622181ed7..7ba81a27e 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -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" diff --git a/packages/angular/src/lucide-icon-base.ts b/packages/angular/src/lucide-icon-base.ts index 0775d0fbf..59fdb9667 100644 --- a/packages/angular/src/lucide-icon-base.ts +++ b/packages/angular/src/lucide-icon-base.ts @@ -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, diff --git a/packages/astro/scripts/exportTemplate.mts b/packages/astro/scripts/exportTemplate.mts index c8a41ed3a..1c43db3e4 100644 --- a/packages/astro/scripts/exportTemplate.mts +++ b/packages/astro/scripts/exportTemplate.mts @@ -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}; `; -}); + }, +); diff --git a/packages/astro/src/Icon.astro b/packages/astro/src/Icon.astro index eb504dbad..df27db114 100644 --- a/packages/astro/src/Icon.astro +++ b/packages/astro/src/Icon.astro @@ -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, +}; --- - {iconNode.map(([Tag, attrs]) => )} + {builtIconNode.map(([Tag, attrs]) => )} diff --git a/packages/astro/src/createLucideIcon.ts b/packages/astro/src/createLucideIcon.ts index 7ff99d5ec..22d12e942 100644 --- a/packages/astro/src/createLucideIcon.ts +++ b/packages/astro/src/createLucideIcon.ts @@ -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, $$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'])}` }, diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts index 71f1d527f..d7ce5f411 100644 --- a/packages/astro/src/types.ts +++ b/packages/astro/src/types.ts @@ -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 diff --git a/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap b/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap index a6ff7f2e4..6f9ff2921 100644 --- a/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap +++ b/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap @@ -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" > 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" > 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" > 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" > { 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(); diff --git a/packages/icons/package.json b/packages/icons/package.json index 165cbd181..7f2e917e7 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -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", diff --git a/packages/icons/src/buildLucideDataUri.ts b/packages/icons/src/buildLucideDataUri.ts index 64aa7526c..eaa3dae1d 100644 --- a/packages/icons/src/buildLucideDataUri.ts +++ b/packages/icons/src/buildLucideDataUri.ts @@ -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; diff --git a/packages/icons/src/buildLucideIconElement.ts b/packages/icons/src/buildLucideIconElement.ts index a819d0fde..c79f96ee2 100644 --- a/packages/icons/src/buildLucideIconElement.ts +++ b/packages/icons/src/buildLucideIconElement.ts @@ -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; diff --git a/packages/icons/src/buildLucideIconNode.ts b/packages/icons/src/buildLucideIconNode.ts index a918ffbbd..9e29618dd 100644 --- a/packages/icons/src/buildLucideIconNode.ts +++ b/packages/icons/src/buildLucideIconNode.ts @@ -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; diff --git a/packages/icons/src/buildLucideSvg.ts b/packages/icons/src/buildLucideSvg.ts index 27bca4a91..197e1b626 100644 --- a/packages/icons/src/buildLucideSvg.ts +++ b/packages/icons/src/buildLucideSvg.ts @@ -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('')}`; +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; diff --git a/packages/icons/src/types.ts b/packages/icons/src/types.ts index 563c93fd7..08e15ffa1 100644 --- a/packages/icons/src/types.ts +++ b/packages/icons/src/types.ts @@ -1,43 +1 @@ -export type SVGProps = Record; - -/** - * 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'; diff --git a/packages/icons/tests/buildLucideIconNode.spec.ts b/packages/icons/tests/buildLucideIconNode.spec.ts index dc3345ac5..5af66acbf 100644 --- a/packages/icons/tests/buildLucideIconNode.spec.ts +++ b/packages/icons/tests/buildLucideIconNode.spec.ts @@ -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(); + }); }); diff --git a/packages/lucide-preact/scripts/exportTemplate.mts b/packages/lucide-preact/scripts/exportTemplate.mts index 28cc154bc..8eaa0baa6 100644 --- a/packages/lucide-preact/scripts/exportTemplate.mts +++ b/packages/lucide-preact/scripts/exportTemplate.mts @@ -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}; `; -}); + }, +); diff --git a/packages/lucide-preact/src/Icon.ts b/packages/lucide-preact/src/Icon.ts index 0fb278848..07583e0b6 100644 --- a/packages/lucide-preact/src/Icon.ts +++ b/packages/lucide-preact/src/Icon.ts @@ -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; diff --git a/packages/lucide-preact/src/createLucideIcon.ts b/packages/lucide-preact/src/createLucideIcon.ts index c5c14d4a3..5620bdad5 100644 --- a/packages/lucide-preact/src/createLucideIcon.ts +++ b/packages/lucide-preact/src/createLucideIcon.ts @@ -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>( - `lucide-${toKebabCase(toPascalCase(iconName))}`, - `lucide-${toKebabCase(iconName)}`, - classes, - className, - ), + name: iconData.name, + aliases: iconData.aliases, + iconNode: iconData.node, + class: mergeClasses>(classes, className), }, children, ); - Component.displayName = toPascalCase(iconName); + Component.displayName = toPascalCase(iconData.name); return Component; -}; +} export default createLucideIcon; diff --git a/packages/lucide-preact/src/types.ts b/packages/lucide-preact/src/types.ts index 03604e960..f9736e37e 100644 --- a/packages/lucide-preact/src/types.ts +++ b/packages/lucide-preact/src/types.ts @@ -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][]; +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> { color?: string; diff --git a/packages/lucide-preact/tests/__snapshots__/context.spec.tsx.snap b/packages/lucide-preact/tests/__snapshots__/context.spec.tsx.snap index b572910a9..85a5974f2 100644 --- a/packages/lucide-preact/tests/__snapshots__/context.spec.tsx.snap +++ b/packages/lucide-preact/tests/__snapshots__/context.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = `
{ - 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}; `; -}); + }, +); diff --git a/packages/vue/src/Icon.ts b/packages/vue/src/Icon.ts index 75d85aee4..5bdecdb5f 100644 --- a/packages/vue/src/Icon.ts +++ b/packages/vue/src/Icon.ts @@ -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 = ( { name, + aliases, iconNode, absoluteStrokeWidth, 'absolute-stroke-width': absoluteStrokeWidthKebabCase, @@ -54,25 +56,39 @@ const Icon: FunctionalComponent = ( 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; diff --git a/packages/vue/src/createLucideIcon.ts b/packages/vue/src/createLucideIcon.ts index 85919b106..10d5247df 100644 --- a/packages/vue/src/createLucideIcon.ts +++ b/packages/vue/src/createLucideIcon.ts @@ -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, ); diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index a8b865db6..38d5f2f6d 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -8,4 +8,9 @@ export interface LucideProps extends Partial { } export type IconNode = [elementName: string, attrs: Record][]; +export interface LucideIconData { + name: string; + node: IconNode; + aliases?: string[]; +} export type LucideIcon = FunctionalComponent; diff --git a/packages/vue/tests/__snapshots__/Icon.spec.ts.snap b/packages/vue/tests/__snapshots__/Icon.spec.ts.snap index 4b742c143..9907eb584 100644 --- a/packages/vue/tests/__snapshots__/Icon.spec.ts.snap +++ b/packages/vue/tests/__snapshots__/Icon.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = ` should render the icon with LucideProvider 1`] = ` should add a class to the element 1`] = `
should add a class to the element 1`] = exports[`Using lucide icon components > should add a style attribute to the element 1`] = `
should add a style attribute to the elem exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = `
should adjust the size, stroke color and exports[`Using lucide icon components > should pass children to the icon slot 1`] = `
should pass children to the icon slot 1` exports[`Using lucide icon components > should render an component 1`] = `
{ expect(IconComponent).toHaveAttribute( 'class', - 'lucide provider-class lucide-house-icon lucide-house icon-class', + 'lucide lucide-house-icon lucide-home provider-class icon-class', ); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ae7209cf..39ee05ff8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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)