mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-08-29 12:18:23 +02:00
* chore: upgrade eslint to v9 latest with compatible lint deps Agent-Logs-Url: https://github.com/lucide-icons/lucide/sessions/f7c1f1b5-d213-4afa-91a8-b799a16397ec * Fix eslint config * Fix a lot of eslint errors * Fix eslint * Fix eslint errors * Fix eslint errors * Fix types * Fix eslint warning * Format code * Add eslint 10 * Remove unused vars * Update pnpm * Update deps --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
505 lines
13 KiB
TypeScript
505 lines
13 KiB
TypeScript
import React from 'react';
|
|
import { PathProps, Path } from './types';
|
|
import getPaths, { assert } from './utils';
|
|
import { GapViolationHighlight } from './GapViolationHighlight.tsx';
|
|
|
|
export const darkModeCss = `
|
|
@media screen and (prefers-color-scheme: light) {
|
|
.svg-preview-grid-rect { fill: none }
|
|
}
|
|
@media screen and (prefers-color-scheme: dark) {
|
|
.svg-preview-grid-rect { fill: none }
|
|
.svg
|
|
.svg-preview-grid-group,
|
|
.svg-preview-radii-group,
|
|
.svg-preview-shadow-mask-group,
|
|
.svg-preview-shadow-group {
|
|
stroke: #fff;
|
|
}
|
|
}
|
|
`;
|
|
|
|
export const Grid = ({
|
|
radius,
|
|
fill,
|
|
height,
|
|
width,
|
|
subGridSize = 0,
|
|
...props
|
|
}: {
|
|
height: number;
|
|
width: number;
|
|
strokeWidth: number;
|
|
subGridSize?: number;
|
|
radius: number;
|
|
} & PathProps<'stroke', 'strokeWidth'>) => (
|
|
<g
|
|
className="svg-preview-grid-group"
|
|
strokeLinecap="butt"
|
|
{...props}
|
|
>
|
|
<rect
|
|
className="svg-preview-grid-rect"
|
|
width={width - props.strokeWidth}
|
|
height={height - props.strokeWidth}
|
|
x={props.strokeWidth / 2}
|
|
y={props.strokeWidth / 2}
|
|
rx={radius}
|
|
fill={fill}
|
|
/>
|
|
<path
|
|
strokeDasharray={
|
|
'0 0.1 ' + '0.1 0.15 '.repeat(subGridSize ? subGridSize * 4 - 1 : 95) + '0 0.15'
|
|
}
|
|
strokeWidth={0.1}
|
|
d={
|
|
props.d ||
|
|
[
|
|
...new Array(Math.floor(width - 1))
|
|
.fill(null)
|
|
.map((_, i) => i)
|
|
.filter((i) => !subGridSize || i % subGridSize !== subGridSize - 1)
|
|
.flatMap((i) => [`M${i + 1} ${props.strokeWidth}v${height - props.strokeWidth * 2}`]),
|
|
...new Array(Math.floor(height - 1))
|
|
.fill(null)
|
|
.map((_, i) => i)
|
|
.filter((i) => !subGridSize || i % subGridSize !== subGridSize - 1)
|
|
.flatMap((i) => [`M${props.strokeWidth} ${i + 1}h${width - props.strokeWidth * 2}`]),
|
|
].join('')
|
|
}
|
|
/>
|
|
{!!subGridSize && (
|
|
<path
|
|
d={
|
|
props.d ||
|
|
[
|
|
...new Array(Math.floor(width - 1))
|
|
.fill(null)
|
|
.map((_, i) => i)
|
|
.filter((i) => i % subGridSize === subGridSize - 1)
|
|
.flatMap((i) => [`M${i + 1} ${props.strokeWidth}v${height - props.strokeWidth * 2}`]),
|
|
...new Array(Math.floor(height - 1))
|
|
.fill(null)
|
|
.map((_, i) => i)
|
|
.filter((i) => i % subGridSize === subGridSize - 1)
|
|
.flatMap((i) => [`M${props.strokeWidth} ${i + 1}h${width - props.strokeWidth * 2}`]),
|
|
].join('')
|
|
}
|
|
/>
|
|
)}
|
|
</g>
|
|
);
|
|
|
|
const Shadow = ({
|
|
radius,
|
|
paths,
|
|
...props
|
|
}: {
|
|
radius: number;
|
|
paths: Path[];
|
|
} & PathProps<'stroke' | 'strokeWidth' | 'strokeOpacity', 'd'>) => {
|
|
const groupedPaths = Object.entries(
|
|
paths.reduce(
|
|
(groups, val) => {
|
|
const key = val.c.id;
|
|
groups[key] = [...(groups[key] || []), val];
|
|
return groups;
|
|
},
|
|
{} as Record<number, Path[]>,
|
|
),
|
|
);
|
|
return (
|
|
<>
|
|
<g
|
|
className="svg-preview-shadow-mask-group"
|
|
{...props}
|
|
>
|
|
{groupedPaths.map(([id, paths]) => (
|
|
<mask
|
|
key={`svg-preview-shadow-mask-${id}`}
|
|
id={`svg-preview-shadow-mask-${id}`}
|
|
maskUnits="userSpaceOnUse"
|
|
strokeOpacity="1"
|
|
strokeWidth={props.strokeWidth}
|
|
stroke="#000"
|
|
>
|
|
<rect
|
|
x={0}
|
|
y={0}
|
|
width="100%"
|
|
height="100%"
|
|
fill="#fff"
|
|
stroke="none"
|
|
rx={radius}
|
|
/>
|
|
<path
|
|
d={paths
|
|
.flatMap(({ prev, next }) => [
|
|
`M${prev.x} ${prev.y}h.01`,
|
|
`M${next.x} ${next.y}h.01`,
|
|
])
|
|
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
|
.join('')}
|
|
/>
|
|
</mask>
|
|
))}
|
|
</g>
|
|
<g
|
|
className="svg-preview-shadow-group"
|
|
{...props}
|
|
>
|
|
{paths.map(({ d, c: { id } }, i) => (
|
|
<path
|
|
key={i}
|
|
mask={`url(#svg-preview-shadow-mask-${id})`}
|
|
d={d}
|
|
/>
|
|
))}
|
|
<path
|
|
d={paths
|
|
.flatMap(({ prev, next }) => [`M${prev.x} ${prev.y}h.01`, `M${next.x} ${next.y}h.01`])
|
|
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
|
.join('')}
|
|
/>
|
|
</g>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const ColoredPath = ({
|
|
colors,
|
|
paths,
|
|
...props
|
|
}: { paths: Path[]; colors: string[] } & PathProps<never, 'd' | 'stroke'>) => {
|
|
let idx = 0;
|
|
return (
|
|
<g
|
|
className="svg-preview-colored-path-group"
|
|
{...props}
|
|
>
|
|
{paths.map(({ d, c }, i) => (
|
|
<path
|
|
key={i}
|
|
d={d}
|
|
stroke={colors[(c.name === 'path' ? idx++ : c.id) % colors.length]}
|
|
/>
|
|
))}
|
|
</g>
|
|
);
|
|
};
|
|
|
|
const ControlPath = ({
|
|
paths,
|
|
radius,
|
|
pointSize,
|
|
...props
|
|
}: {
|
|
pointSize: number;
|
|
paths: Path[];
|
|
radius: number;
|
|
} & PathProps<'stroke' | 'strokeWidth', 'd'>) => {
|
|
const controlPaths = paths.map((path, i) => {
|
|
const element = paths.filter((p) => p.c.id === path.c.id);
|
|
const lastElement = element.at(-1)?.next;
|
|
assert(lastElement);
|
|
const isClosed = element[0].prev.x === lastElement.x && element[0].prev.y === lastElement.y;
|
|
const showMarker = !['rect', 'circle', 'ellipse'].includes(path.c.name);
|
|
return {
|
|
...path,
|
|
showMarker,
|
|
startMarker: showMarker && path.isStart && !isClosed,
|
|
endMarker: showMarker && paths[i + 1]?.isStart !== false && !isClosed,
|
|
};
|
|
});
|
|
return (
|
|
<>
|
|
<g
|
|
className="svg-preview-control-path-marker-mask-group"
|
|
strokeWidth={pointSize}
|
|
stroke="#000"
|
|
>
|
|
{controlPaths.map(({ prev, next, showMarker }, i) => {
|
|
return (
|
|
showMarker && (
|
|
<mask
|
|
id={`svg-preview-control-path-marker-mask-${i}`}
|
|
key={i}
|
|
maskUnits="userSpaceOnUse"
|
|
>
|
|
<rect
|
|
x="0"
|
|
y="0"
|
|
width="100%"
|
|
height="100%"
|
|
fill="#fff"
|
|
stroke="none"
|
|
rx={radius}
|
|
/>
|
|
<path d={`M${prev.x} ${prev.y}h.01`} />
|
|
<path d={`M${next.x} ${next.y}h.01`} />
|
|
</mask>
|
|
)
|
|
);
|
|
})}
|
|
</g>
|
|
<g
|
|
className="svg-preview-control-path-group"
|
|
{...props}
|
|
>
|
|
{controlPaths.map(({ d, showMarker }, i) => (
|
|
<path
|
|
key={i}
|
|
mask={showMarker ? `url(#svg-preview-control-path-marker-mask-${i})` : undefined}
|
|
d={d}
|
|
/>
|
|
))}
|
|
</g>
|
|
<g
|
|
className="svg-preview-control-path-marker-group"
|
|
{...props}
|
|
>
|
|
<path
|
|
d={controlPaths
|
|
.flatMap(({ prev, next, showMarker }) =>
|
|
showMarker ? [`M${prev.x} ${prev.y}h.01`, `M${next.x} ${next.y}h.01`] : [],
|
|
)
|
|
.join('')}
|
|
/>
|
|
{controlPaths.map(({ prev, next, startMarker, endMarker }, i) => (
|
|
<React.Fragment key={i}>
|
|
{startMarker && (
|
|
<circle
|
|
cx={prev.x}
|
|
cy={prev.y}
|
|
r={pointSize / 2}
|
|
/>
|
|
)}
|
|
{endMarker && (
|
|
<circle
|
|
cx={next.x}
|
|
cy={next.y}
|
|
r={pointSize / 2}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</g>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Radii = ({
|
|
paths,
|
|
...props
|
|
}: { paths: Path[] } & PathProps<
|
|
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
|
|
never
|
|
>) => {
|
|
return (
|
|
<g
|
|
className="svg-preview-radii-group"
|
|
{...props}
|
|
>
|
|
{paths.map(
|
|
({ circle, next, prev, c }, i) =>
|
|
circle && (
|
|
<React.Fragment key={i}>
|
|
{circle.tangentIntersection && c.name === 'path' && (
|
|
<>
|
|
<circle
|
|
cx={next.x * 2 - circle.tangentIntersection.x}
|
|
cy={next.y * 2 - circle.tangentIntersection.y}
|
|
r={0.25}
|
|
/>
|
|
<circle
|
|
cx={prev.x * 2 - circle.tangentIntersection.x}
|
|
cy={prev.y * 2 - circle.tangentIntersection.y}
|
|
r={0.25}
|
|
/>
|
|
<path
|
|
d={`M${next.x * 2 - circle.tangentIntersection.x} ${
|
|
next.y * 2 - circle.tangentIntersection.y
|
|
}L${circle.tangentIntersection.x} ${circle.tangentIntersection.y}L${prev.x * 2 - circle.tangentIntersection.x} ${
|
|
prev.y * 2 - circle.tangentIntersection.y
|
|
}`}
|
|
/>
|
|
<circle
|
|
cx={circle.tangentIntersection.x}
|
|
cy={circle.tangentIntersection.y}
|
|
r={0.25}
|
|
/>
|
|
</>
|
|
)}
|
|
{c.name === 'path' && (
|
|
<path d={`M${next.x} ${next.y}L${circle.x} ${circle.y}L${prev.x} ${prev.y}`} />
|
|
)}
|
|
<circle
|
|
cy={circle.y}
|
|
cx={circle.x}
|
|
r={0.25}
|
|
strokeDasharray="0"
|
|
stroke={
|
|
(Math.round(circle.x * 100) / 100) % 1 !== 0 ||
|
|
(Math.round(circle.y * 100) / 100) % 1 !== 0
|
|
? 'red'
|
|
: undefined
|
|
}
|
|
/>
|
|
<circle
|
|
cy={circle.y}
|
|
cx={circle.x}
|
|
r={circle.r}
|
|
stroke={(Math.round(circle.r * 1000) / 1000) % 1 !== 0 ? 'red' : undefined}
|
|
/>
|
|
</React.Fragment>
|
|
),
|
|
)}
|
|
</g>
|
|
);
|
|
};
|
|
|
|
const Handles = ({
|
|
paths,
|
|
...props
|
|
}: { paths: Path[] } & PathProps<'strokeWidth' | 'stroke' | 'strokeOpacity', never>) => (
|
|
<g
|
|
className="svg-preview-handles-group"
|
|
{...props}
|
|
>
|
|
{paths.map(({ prev, next, cp1, cp2 }, i) => (
|
|
<React.Fragment key={i}>
|
|
{cp1 && <path d={`M${prev.x} ${prev.y} ${cp1.x} ${cp1.y}`} />}
|
|
{cp1 && (
|
|
<circle
|
|
cy={cp1.y}
|
|
cx={cp1.x}
|
|
r={0.25}
|
|
/>
|
|
)}
|
|
{cp2 && <path d={`M${next.x} ${next.y} ${cp2.x} ${cp2.y}`} />}
|
|
{cp2 && (
|
|
<circle
|
|
cy={cp2.y}
|
|
cx={cp2.x}
|
|
r={0.25}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</g>
|
|
);
|
|
|
|
interface SvgPreviewProps extends React.SVGProps<SVGSVGElement> {
|
|
height?: number;
|
|
width?: number;
|
|
src: string | ReturnType<typeof getPaths>;
|
|
showGrid?: boolean;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
const SvgPreview = (
|
|
{ src, children, height = 24, width = 24, showGrid = false, ...props }: SvgPreviewProps,
|
|
ref,
|
|
) => {
|
|
const subGridSize =
|
|
Math.max(height, width) % 3 === 0
|
|
? Math.max(height, width) > 24
|
|
? 12
|
|
: 3
|
|
: Math.max(height, width) % 5 === 0
|
|
? 5
|
|
: 0;
|
|
const paths = typeof src === 'string' ? getPaths(src) : src;
|
|
|
|
return (
|
|
<svg
|
|
ref={ref}
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width={width}
|
|
height={height}
|
|
viewBox={`0 0 ${width} ${height}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
{...props}
|
|
>
|
|
<style>{darkModeCss}</style>
|
|
{showGrid && (
|
|
<Grid
|
|
height={height}
|
|
width={width}
|
|
subGridSize={subGridSize}
|
|
strokeWidth={0.1}
|
|
stroke="#777"
|
|
mask="url(#svg-preview-bounding-box-mask)"
|
|
strokeOpacity={0.3}
|
|
radius={1}
|
|
/>
|
|
)}
|
|
<Shadow
|
|
paths={paths}
|
|
strokeWidth={4}
|
|
stroke="#777"
|
|
radius={1}
|
|
strokeOpacity={0.15}
|
|
/>
|
|
<GapViolationHighlight
|
|
paths={paths}
|
|
stroke="red"
|
|
strokeOpacity={0.75}
|
|
strokeWidth={4}
|
|
/>
|
|
<Handles
|
|
paths={paths}
|
|
strokeWidth={0.12}
|
|
stroke="#777"
|
|
strokeOpacity={0.6}
|
|
/>
|
|
<ColoredPath
|
|
paths={paths}
|
|
colors={[
|
|
'#1982c4',
|
|
'#4267AC',
|
|
'#6a4c93',
|
|
'#B55379',
|
|
'#FF595E',
|
|
'#FF7655',
|
|
'#ff924c',
|
|
'#FFAE43',
|
|
'#ffca3a',
|
|
'#C5CA30',
|
|
'#8ac926',
|
|
'#52A675',
|
|
]}
|
|
/>
|
|
<Radii
|
|
paths={paths}
|
|
strokeWidth={0.12}
|
|
strokeDasharray="0 0.25 0.25"
|
|
stroke="#777"
|
|
strokeOpacity={0.3}
|
|
/>
|
|
<ControlPath
|
|
radius={1}
|
|
paths={paths}
|
|
pointSize={1}
|
|
stroke="#fff"
|
|
strokeWidth={0.125}
|
|
/>
|
|
<Handles
|
|
paths={paths}
|
|
strokeWidth={0.12}
|
|
stroke="#FFF"
|
|
strokeOpacity={0.3}
|
|
/>
|
|
{children}
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
SvgPreview.displayName = 'SvgPreview';
|
|
|
|
export default SvgPreview;
|