mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-12 13:38:32 +00:00
feat(): unify icon build logic some more, add nonScalingStroke option, marking absoluteStrokeWidth as deprecated
This commit is contained in:
@@ -21,7 +21,7 @@ const {
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
const [, builtIconAttributes, builtIconNode] = buildLucideIconNode(icon, {
|
||||
const [, builtIconAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
|
||||
color,
|
||||
width: width ?? size,
|
||||
height: height ?? size,
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { h, toChildArray } from 'preact';
|
||||
import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared';
|
||||
import defaultAttributes from './defaultAttributes';
|
||||
import type { LucideIconData, LucideIconNode, LucideProps } from './types';
|
||||
import { useLucideContext } from './context';
|
||||
|
||||
type IconComponentProps = LucideProps &
|
||||
(
|
||||
| {
|
||||
iconNode: LucideIconNode;
|
||||
icon?: never;
|
||||
}
|
||||
| {
|
||||
icon: LucideIconData;
|
||||
iconNode?: never;
|
||||
}
|
||||
| {
|
||||
iconNode: LucideIconNode[];
|
||||
icon?: never;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -59,7 +58,7 @@ const Icon = ({
|
||||
class: contextClass = '',
|
||||
} = useLucideContext() ?? {};
|
||||
|
||||
const [name, svgAttributes, builtIconNode] = buildLucideIconNode(icon, {
|
||||
const [name, svgAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
|
||||
color: color ?? contextColor,
|
||||
width: width ?? size ?? contextSize,
|
||||
height: height ?? size ?? contextSize,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type FunctionComponent, type JSX } from 'preact';
|
||||
import type {
|
||||
LucideIconData as SharedLucideIconData,
|
||||
LucideIconNode as SharedLucideIconNode,
|
||||
} from '@lucide/shared';
|
||||
} from '@lucide/shared/types';
|
||||
|
||||
export type LucideIconNode = SharedLucideIconNode;
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
|
||||
|
||||
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
|
||||
|
||||
const [, svgAttributes, builtIconNode] = buildLucideIconForReact(icon, {
|
||||
const [, svgAttributes, builtIconNode = []] = buildLucideIconForReact(icon, {
|
||||
color: color ?? contextColor,
|
||||
width: width ?? size ?? contextSize,
|
||||
height: height ?? size ?? contextSize,
|
||||
|
||||
@@ -66,7 +66,7 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
|
||||
|
||||
const hasAccessibleProp = Boolean(children) || hasA11yProp(rest);
|
||||
|
||||
const [name, svgAttributes, builtIconNode] = buildLucideIconForReact(icon, {
|
||||
const [name, svgAttributes, builtIconNode = []] = buildLucideIconForReact(icon, {
|
||||
color: color ?? contextColor,
|
||||
width: width ?? size ?? contextSize,
|
||||
height: height ?? size ?? contextSize,
|
||||
|
||||
@@ -39,7 +39,7 @@ const Icon = (props: LucideProps & IconProps) => {
|
||||
aliases: [],
|
||||
};
|
||||
|
||||
const [, svgAttributes, builtIconNode] = buildLucideIconNode(icon, {
|
||||
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
|
||||
color: localProps.color ?? globalProps.color,
|
||||
width: localProps.width ?? localProps.size ?? globalProps.size,
|
||||
height: localProps.height ?? localProps.size ?? globalProps.size,
|
||||
|
||||
@@ -17,8 +17,8 @@ function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {
|
||||
const viewBoxHeight = icon.size ?? icon.height ?? defaultAttributes['height'];
|
||||
const aliasClassNames =
|
||||
icon.aliases
|
||||
?.filter((alias): alias is string => typeof alias === 'string' && alias.trim() !== '')
|
||||
.map((alias) => `lucide-${alias}`) ?? [];
|
||||
?.filter((alias: string): alias is string => typeof alias === 'string' && alias.trim() !== '')
|
||||
.map((alias: string) => `lucide-${alias}`) ?? [];
|
||||
const iconClassNames = [...(icon.name ? [`lucide-${icon.name}`] : []), ...aliasClassNames];
|
||||
const classNamesFromClassName = params.className?.split(' ').filter(Boolean) ?? [];
|
||||
const className =
|
||||
@@ -55,12 +55,14 @@ function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {
|
||||
return [
|
||||
'svg',
|
||||
attributes,
|
||||
icon.node.map(([name, attrs]) => [
|
||||
name,
|
||||
params.nonScalingStroke
|
||||
icon.node.map((child): LucideIconNode => {
|
||||
const [name, attrs, children] = child;
|
||||
const nextAttrs = params.nonScalingStroke
|
||||
? { [getAttributeName('vector-effect')]: 'non-scaling-stroke', ...attrs }
|
||||
: attrs,
|
||||
]),
|
||||
: attrs;
|
||||
|
||||
return children ? [name, nextAttrs, children] : [name, nextAttrs];
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type SVGProps = Record<string, unknown>;
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type SVGProps = Record<string, any>;
|
||||
|
||||
/**
|
||||
* A Lucide icon node (an svgson-like internal format)
|
||||
@@ -70,6 +71,6 @@ export type LucideBuildParams<TProps extends Record<string, unknown> = SVGProps>
|
||||
*/
|
||||
attributeNames?: Record<string, string>;
|
||||
} & (
|
||||
| { size?: string | number; width?: never; height?: never }
|
||||
| { size?: never; width?: string | number; height?: string | number }
|
||||
| { size?: string | number | null; width?: never; height?: never }
|
||||
| { size?: never; width?: string | number | null; height?: string | number | null }
|
||||
);
|
||||
|
||||
@@ -4,7 +4,24 @@
|
||||
* @param {string} string
|
||||
* @returns {string} A camelized string
|
||||
*/
|
||||
export const toCamelCase = <T extends string>(string: T) =>
|
||||
string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) =>
|
||||
p2 ? p2.toUpperCase() : p1.toLowerCase(),
|
||||
);
|
||||
export const toCamelCase = <T extends string>(string: T) => {
|
||||
let out = '';
|
||||
let upperNext = false;
|
||||
|
||||
for (const ch of string) {
|
||||
if (ch === '-' || ch === '_' || ch <= ' ') {
|
||||
upperNext = out.length > 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (out.length === 0) {
|
||||
out += ch.toLowerCase();
|
||||
} else {
|
||||
out += upperNext ? ch.toUpperCase() : ch;
|
||||
}
|
||||
|
||||
upperNext = false;
|
||||
}
|
||||
|
||||
return out as string;
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"scripts": {
|
||||
"build": "pnpm clean && pnpm copy:license && pnpm copy:utils && pnpm build:icons && pnpm build:package && pnpm build:license",
|
||||
"copy:license": "cp ../../LICENSE ./LICENSE",
|
||||
"copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/{hasA11yProp.ts,mergeClasses.ts} ./src/utils/",
|
||||
"copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/{hasA11yProp.ts,mergeClasses.ts} ./src/utils/ && node ./scripts/patchCopiedUtils.mts",
|
||||
"clean": "rm -rf dist stats ./src/icons/*.{ts,svelte} ./src/aliases/{aliases,prefixed,suffixed}.ts",
|
||||
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --exportFileName=index.ts --iconFileExtension=.svelte --importImportFileExtension=.svelte --separateIconFileExport --separateIconFileExportExtension=.ts --withAliases --aliasesFileExtension=.ts --separateAliasesFile --separateAliasesFileExtension=.ts --aliasImportFileExtension=.js --pretty=false",
|
||||
"build:package": "svelte-package --input ./src",
|
||||
|
||||
@@ -8,8 +8,8 @@ export default defineExportTemplate(
|
||||
const svgBase64 = base64SVG(svgContents);
|
||||
|
||||
return `\
|
||||
<script lang="ts">
|
||||
${getHTMLBanner()}
|
||||
<script lang="ts">
|
||||
import Icon from '../Icon.svelte';
|
||||
import type { LucideProps, LucideIconData } from '../types.js';
|
||||
|
||||
@@ -29,7 +29,7 @@ Lucide SVG icon component, renders SVG Element with children.
|
||||
${deprecated ? `\n@deprecated ${deprecationReason}\n` : ''}\
|
||||
-->
|
||||
|
||||
<Icon icon={iconData} {...props} />
|
||||
<Icon {...props} icon={iconData} />
|
||||
`;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
const filePath = new URL('../src/utils/buildLucideIconNode.ts', import.meta.url);
|
||||
|
||||
const source = await readFile(filePath, 'utf8');
|
||||
|
||||
const patched = source
|
||||
.replace("from './types';", "from './types.js';")
|
||||
.replace("from './defaultAttributes';", "from './defaultAttributes.js';")
|
||||
.replace("from '../utils/mergeClasses';", "from '../utils/mergeClasses.js';");
|
||||
|
||||
if (patched !== source) {
|
||||
await writeFile(filePath, patched);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import buildLucideIconNode from './utils/buildLucideIconNode.js';
|
||||
import type {IconProps} from './types.js';
|
||||
import { hasA11yProp } from './utils/hasA11yProp.js';
|
||||
import { getLucideContext } from './context.js';
|
||||
import {hasA11yProp} from './utils/hasA11yProp.js';
|
||||
import {getLucideContext} from './context.js';
|
||||
import {mergeClasses} from './utils/mergeClasses.js';
|
||||
import type {ClassValue} from "svelte/elements";
|
||||
import type {ClassValue} from 'svelte/elements';
|
||||
|
||||
const globalProps = getLucideContext() ?? {};
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
Boolean(children) || hasA11yProp(props as Record<string, any>),
|
||||
);
|
||||
|
||||
const [, svgAttributes, builtIconNode] = $derived(
|
||||
const [, svgAttributes, builtIconNode = []] = $derived(
|
||||
buildLucideIconNode(
|
||||
icon,
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
LucideIconNode as SharedLucideIconNode,
|
||||
} from '@lucide/shared/types';
|
||||
|
||||
export type Attrs = SVGAttributes<SVGSVGElement>;
|
||||
export type Attrs = Record<string, unknown> & SVGAttributes<SVGSVGElement>;
|
||||
type IconNodeElements =
|
||||
| 'circle'
|
||||
| 'ellipse'
|
||||
|
||||
@@ -114,6 +114,46 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewbox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="red"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-smile lucide-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle vector-effect="non-scaling-stroke"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
>
|
||||
</circle>
|
||||
<path vector-effect="non-scaling-stroke"
|
||||
d="M8 14s1.5 2 4 2 4-2 4-2"
|
||||
>
|
||||
</path>
|
||||
<line vector-effect="non-scaling-stroke"
|
||||
x1="9"
|
||||
x2="9.01"
|
||||
y1="9"
|
||||
y2="9"
|
||||
>
|
||||
</line>
|
||||
<line vector-effect="non-scaling-stroke"
|
||||
x1="15"
|
||||
x2="15.01"
|
||||
y1="9"
|
||||
y2="9"
|
||||
>
|
||||
</line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`Using lucide icon components > should render an component 1`] = `
|
||||
<div>
|
||||
<svg
|
||||
|
||||
@@ -83,6 +83,24 @@ describe('Using lucide icon components', () => {
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should apply vector-effect when nonScalingStroke is set', () => {
|
||||
const { container } = render(Smile, {
|
||||
color: 'red',
|
||||
size: 48,
|
||||
nonScalingStroke: true,
|
||||
});
|
||||
|
||||
const IconComponent = container.firstElementChild;
|
||||
|
||||
expect(IconComponent).toHaveAttribute('width', '48');
|
||||
expect(IconComponent).toHaveAttribute('height', '48');
|
||||
expect(IconComponent).toHaveAttribute('stroke', 'red');
|
||||
expect(IconComponent).toHaveAttribute('stroke-width', '2');
|
||||
expect(IconComponent?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke');
|
||||
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should use context values from the global set properties', () => {
|
||||
const { container } = render(ContextWrapper);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
mergeClasses,
|
||||
toKebabCase,
|
||||
} from '@lucide/shared';
|
||||
import defaultAttributes from './defaultAttributes';
|
||||
import { LucideIconNode, LucideIconData, LucideProps } from './types';
|
||||
import { useLucideProps } from './context';
|
||||
|
||||
@@ -72,15 +71,11 @@ const Icon: FunctionalComponent<LucideProps & IconProps> = (
|
||||
|
||||
const { class: propsClass, ...restProps } = props;
|
||||
|
||||
const [, svgAttributes, builtIconNode] = buildLucideIconNode(icon, {
|
||||
const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(icon, {
|
||||
color: color ?? contextColor,
|
||||
width: width ?? size ?? contextSize,
|
||||
height: height ?? size ?? contextSize,
|
||||
strokeWidth:
|
||||
strokeWidth ??
|
||||
strokeWidthKebabCase ??
|
||||
contextStrokeWidth ??
|
||||
defaultAttributes['stroke-width'],
|
||||
strokeWidth: strokeWidth ?? strokeWidthKebabCase ?? contextStrokeWidth,
|
||||
absoluteStrokeWidth: isAbsoluteStrokeWidth,
|
||||
nonScalingStroke: isNonScalingStroke,
|
||||
className: mergeClasses(contextClass, propsClass),
|
||||
|
||||
@@ -21,19 +21,6 @@ describe('Using Icon Component', () => {
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render icon based on icon data', async () => {
|
||||
const { container } = render(Icon, {
|
||||
props: {
|
||||
icon: airVentIcon,
|
||||
size: 48,
|
||||
color: 'red',
|
||||
absoluteStrokeWidth: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render icon and match snapshot', async () => {
|
||||
const { container } = render(Icon, {
|
||||
props: {
|
||||
@@ -46,16 +33,4 @@ describe('Using Icon Component', () => {
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render icon based on icon data', async () => {
|
||||
const { container } = render(Icon, {
|
||||
props: {
|
||||
icon: airVentIcon,
|
||||
size: 48,
|
||||
color: 'red',
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.firstChild).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,31 +55,3 @@ exports[`Using Icon Component > should render icon based on an icon node 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
exports[`Using Icon Component > should render icon based on icon data 1`] = `
|
||||
<svg
|
||||
class="lucide lucide-air-vent"
|
||||
fill="none"
|
||||
height="48"
|
||||
stroke="red"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
viewBox="0 0 24 24"
|
||||
width="48"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="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>
|
||||
`;
|
||||
|
||||
Generated
+300
-227
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user