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); }