/**
* Icon helper for VMK UI Kit.
*
* Loads SVG icons from /assets/icons/svg/ by name and renders them inline.
* Icon names match keys in /assets/icons/index.json.
*
* Usage:
* iconNode("arrows/arrow/right") -> SVGElement
* iconHtml("essentials/user") -> string
*/
const indexUrl = "/assets/icons/index.json";
let indexPromise = null;
function loadIndex() {
if (indexPromise) return indexPromise;
indexPromise = fetch(indexUrl)
.then(r => {
if (!r.ok) throw new Error(`Failed to load icon index: ${r.status}`);
return r.json();
})
.catch(err => {
console.error(err);
return {};
});
return indexPromise;
}
function svgUrl(relativePath) {
return `/assets/icons/svg/${relativePath}`;
}
function sanitizeSvg(svgText) {
// Keep only <svg> element and its children; strip scripts, events, styles.
return svgText
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/on\w+="[^"]*"/gi, "")
.replace(/on\w+='[^']*'/gi, "")
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<!--[\s\S]*?-->/g, "");
}
export async function iconSvg(name) {
const index = await loadIndex();
const rel = index[name];
if (!rel) {
console.warn(`Icon not found: ${name}`);
return "";
}
const res = await fetch(svgUrl(rel));
if (!res.ok) {
console.warn(`Failed to fetch icon: ${name}`);
return "";
}
return sanitizeSvg(await res.text());
}
export async function iconNode(name) {
const svgText = await iconSvg(name);
if (!svgText) return null;
const wrapper = document.createElement("span");
wrapper.innerHTML = svgText;
const svg = wrapper.querySelector("svg");
if (!svg) return null;
svg.setAttribute("aria-hidden", "true");
svg.classList.add("vmk-icon");
return svg;
}
export function iconHtml(name) {
return iconSvg(name);
}