import React, {createContext, ReactElement, SVGProps, use} from "react";
import parse, { HTMLReactParserOptions, Element } from "html-react-parser";
import * as cn from "./Icon.styles"
// TODO: html-react-parser is not the best choice, but it works for now
export const IconProvider = createContext({
iconSet: {}
})
interface IconProps {
name: string;
size?: number;
color?: string;
}
/** Whether an icon `name` is actually an image SOURCE (path / URL / data-uri /
* image file) rather than an icon-set key — then it renders as an `
`. */
export function isImageSource(name: string): boolean {
if (!name) return false
return /^(\/|https?:|data:)/.test(name) || /\.(svg|png|jpe?g|webp|gif)$/i.test(name)
}
export function Icon(props: IconProps): ReactElement | null {
const {name, size = 24, color} = props
const iconProvider = use(IconProvider)
// A path/URL/data-uri icon (e.g. a product logo "/nomad.svg") renders as an
// image, keeping its intrinsic aspect ratio (logos are usually not square).
if (isImageSource(name)) {
return
}
const iconSet = iconProvider?.iconSet
if (!iconSet) {
return null
}
const ico = iconSet[name]
if (!ico || !ico.svg) {
return null
}
const icon = parseIcon(ico.svg) as ReactElement>
if (React.isValidElement(icon)) {
return React.cloneElement(icon, {
width: size,
height: size,
color: color || icon.props.color || 'currentColor',
fill: color || icon.props.fill || 'currentColor',
style: {
width: size,
height: size,
color: color || icon.props.color || 'currentColor',
fill: color || icon.props.color || 'currentColor',
}
})
}
return null
}
Icon.ExternalArrow = function ExternalArrow() {
return
}
function parseIcon(svg: string) {
const options: HTMLReactParserOptions = {
htmlparser2: { xmlMode: true }, // preserves case of *attributes*, still lowercase tags
replace(domNode) {
if (domNode.type === "tag") {
const el = domNode as Element;
// ✅ Fix tag name casing
if (el.name === "lineargradient") el.name = "linearGradient";
if (el.name === "radialgradient") el.name = "radialGradient";
if (el.name === "clippath") el.name = "clipPath";
if (el.name === "pattern") el.name = "pattern"; // example: stays same
// ✅ Fix attribute casing for React
const attrs = el.attribs || {};
if ("stop-color" in attrs) {
attrs.stopColor = attrs["stop-color"];
delete attrs["stop-color"];
}
if ("fill-rule" in attrs) {
attrs.fillRule = attrs["fill-rule"];
delete attrs["fill-rule"];
}
if ("clip-rule" in attrs) {
attrs.clipRule = attrs["clip-rule"];
delete attrs["clip-rule"];
}
}
return domNode;
},
};
return parse(svg, options);
}