// Shared, map-agnostic helpers for rendering an icon's inner SVG markup // from its raw data — used by both the SSR path (resolve.ts, which // statically imports all 5 generated maps since a single request only ever // needs one library and Node bundle size isn't shipped to the browser) and // the client-side lazy-swap path (client-swap.ts, which dynamically // `import()`s one map at a time — see that file's header for why: shadcn's // own create-icon-loader.tsx lazy-loads every non-default library the same // way, and this file is what keeps the two paths' rendering logic // (wrapper attrs, hugeicons node->HTML, name-suffix fallback) from // drifting apart while their IMPORT strategy differs. import type { IconLibraryName } from "./icon-names.ts"; // [tagName, attributes][] — the shape hugeicons icons are stored in. Declared // here, not imported from the generated hugeicons map, so this file stays valid // when `marko-ui add` copies only the configured library's map. export type IconNode = [string, Record][]; export const FALLBACK_INNER = ''; /** * The `` wrapper attributes a library contributes. * * `stroke-linecap`/`stroke-linejoin` are typed as the SVG enums rather than * `string`, because they are spread straight onto an `` whose attribute * types are the real SVG unions — a `Record` here widened them * to `string` and lost the guarantee at exactly the point it is needed. * Optional keys are marked optional so only the libraries that set them * (phosphor/remixicon set neither) have to. */ interface IconWrapperAttrs { viewBox: string; fill: string; stroke?: string; "stroke-width"?: string; "stroke-linecap"?: "butt" | "round" | "square"; "stroke-linejoin"?: "arcs" | "bevel" | "miter" | "miter-clip" | "round"; } // Per-library wrapper attributes, verified against each installed // package's real SVG output (see build-icons.ts's header comment). export const ICON_WRAPPER_ATTRS: Record = { lucide: { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", }, tabler: { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", }, phosphor: { viewBox: "0 0 256 256", fill: "currentColor", }, remixicon: { viewBox: "0 0 24 24", fill: "currentColor", }, hugeicons: { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", }, }; // camelCase React-DOM attrs (hugeicons' node data) -> kebab-case HTML attrs. // Only the attrs actually present in @hugeicons/core-free-icons nodes need // mapping (verified against the installed package); "key" is React-only // and dropped. function toHtmlAttrString(attrs: Record): string { let out = ""; for (const [rawKey, value] of Object.entries(attrs)) { if (rawKey === "key") continue; const key = rawKey.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); out += ` ${key}="${value}"`; } return out; } export function renderHugeiconsNodes(nodes: IconNode): string { return nodes.map(([tag, attrs]) => `<${tag}${toHtmlAttrString(attrs)}/>`).join(""); } // The generated maps are keyed by shadcn's abstract names exactly as // vendored from their index.json (a mix of "Icon"-suffixed and bare names — // shadcn only aliases both forms for names their own source literally uses // both ways). Consumers here sometimes pass either form (e.g. "SearchIcon" // where the map key is bare "Search"), so try the exact key first, then the // suffix-toggled form, before falling back to the placeholder glyph. export function withSuffixFallback(map: Record, name: string): T | undefined { if (name in map) return map[name]; const toggled = name.endsWith("Icon") ? name.slice(0, -"Icon".length) : `${name}Icon`; return map[toggled]; }