"use strict";
// WebFluent Runtime v1.0
// Fine-grained reactivity + DOM helpers + Router + Store + Fetch
const WF = (() => {
// ─── Reactivity ──────────────────────────────────────
let currentEffect = null;
function signal(value) {
const subs = new Set();
const get = () => {
if (currentEffect) subs.add(currentEffect);
return value;
};
const set = (v) => {
if (typeof v === "function") v = v(value);
if (v !== value) {
value = v;
for (const fn of [...subs]) fn();
}
};
get.set = set;
get.update = (fn) => set(fn(value));
get.subscribe = (fn) => { subs.add(fn); return () => subs.delete(fn); };
return get;
}
function effect(fn) {
const run = () => {
const prev = currentEffect;
currentEffect = run;
try { fn(); } finally { currentEffect = prev; }
};
run();
return run;
}
function computed(fn) {
const s = signal(undefined);
effect(() => s.set(fn()));
return s;
}
// ─── DOM Helpers ─────────────────────────────────────
function h(tag, attrs, ...children) {
const el = document.createElement(tag);
if (attrs) {
for (const [k, v] of Object.entries(attrs)) {
if (k.startsWith("on:")) {
el.addEventListener(k.slice(3), v);
} else if (k === "className" || k === "class") {
if (typeof v === "function") {
effect(() => { el.className = v(); });
} else {
el.className = v;
}
} else if (k === "style" && typeof v === "object") {
Object.assign(el.style, v);
} else if (k === "checked") {
if (typeof v === "function") {
effect(() => { el.checked = v(); });
} else {
el.checked = v;
}
} else if (k === "value") {
if (typeof v === "function") {
effect(() => { el.value = v(); });
} else {
el.value = v;
}
} else if (k === "disabled" || k === "multiple" || k === "required" || k === "readOnly") {
if (typeof v === "function") {
effect(() => { el[k] = !!v(); });
} else {
el[k] = !!v;
}
} else if (k === "min" || k === "max" || k === "step") {
if (typeof v === "function") {
effect(() => { el[k] = String(v()); });
} else {
el[k] = String(v);
}
} else if (k === "data-icon") {
// Render icon as inline SVG or text emoji/symbol
const iconName = typeof v === "function" ? v() : v;
_renderIcon(el, iconName);
} else if (typeof v === "function") {
effect(() => { el.setAttribute(k, v()); });
} else if (v != null && v !== false) {
el.setAttribute(k, v);
}
}
}
appendChildren(el, children);
return el;
}
function appendChildren(el, children) {
for (const child of children.flat(Infinity)) {
if (child == null || child === false) continue;
if (typeof child === "string" || typeof child === "number") {
el.appendChild(document.createTextNode(String(child)));
} else if (child instanceof Node) {
el.appendChild(child);
} else if (typeof child === "function") {
reactiveText(el, child);
}
}
}
function reactiveText(parent, fn) {
const node = document.createTextNode("");
parent.appendChild(node);
effect(() => { node.textContent = String(fn()); });
return node;
}
function text(fn) {
if (typeof fn === "function") {
const node = document.createTextNode("");
effect(() => { node.textContent = String(fn()); });
return node;
}
return document.createTextNode(String(fn));
}
// ─── Animation helpers ──────────────────────────────
const ANIM_REVERSE = {
fadeIn: "fadeOut", fadeOut: "fadeIn",
slideUp: "slideDown", slideDown: "slideUp",
slideLeft: "slideRight", slideRight: "slideLeft",
scaleIn: "scaleOut", scaleOut: "scaleIn",
bounce: "fadeOut", shake: "fadeOut", pulse: "fadeOut",
};
function animateIn(el, name, duration, delay) {
if (!name) return Promise.resolve();
const cls = "wf-animate-" + name;
if (duration) el.style.animationDuration = duration;
if (delay) el.style.animationDelay = delay;
el.classList.add(cls);
return new Promise(resolve => {
const done = () => { el.classList.remove(cls); el.style.animationDuration = ""; el.style.animationDelay = ""; resolve(); };
el.addEventListener("animationend", done, { once: true });
// Fallback timeout
setTimeout(done, (parseInt(duration) || 300) + (parseInt(delay) || 0) + 100);
});
}
function animateOut(el, name, duration) {
if (!name) return Promise.resolve();
const cls = "wf-animate-" + name;
if (duration) el.style.animationDuration = duration;
el.classList.add(cls);
return new Promise(resolve => {
const done = () => { el.classList.remove(cls); el.style.animationDuration = ""; resolve(); };
el.addEventListener("animationend", done, { once: true });
setTimeout(done, (parseInt(duration) || 300) + 100);
});
}
function animateEl(target, name, duration) {
const el = typeof target === "string" ? document.querySelector(`[data-ref="${target}"]`) : target;
if (!el) return;
return animateIn(el, name, duration);
}
function replayAnimation(el, name, duration) {
// Remove then re-add the animation class to restart it
const cls = "wf-animate-" + name;
el.classList.remove(cls);
// Force reflow to reset animation
void el.offsetWidth;
el.classList.add(cls);
if (duration) el.style.animationDuration = duration;
}
// ─── Conditional rendering ───────────────────────────
function removeNodes(nodes) {
for (const n of nodes) {
if (n && n.parentNode) n.parentNode.removeChild(n);
}
}
function condRender(parent, condFn, thenFn, elseFn, animConfig) {
const marker = document.createComment("wf-if");
parent.appendChild(marker);
let currentNodes = [];
let lastShow = undefined;
let pendingRemoval = null; // Track in-progress exit animations
// Only track the condition signal — not signals read during rendering
effect(() => {
const show = !!condFn();
if (show === lastShow) return;
lastShow = show;
// Cancel any pending removal animation
if (pendingRemoval) {
removeNodes(pendingRemoval);
pendingRemoval = null;
}
// Remove old nodes
const toRemove = [...currentNodes];
currentNodes = [];
if (animConfig && animConfig.exit && toRemove.length) {
pendingRemoval = toRemove;
const exitName = animConfig.exit;
const promises = toRemove.map(n =>
n instanceof Element ? animateOut(n, exitName, animConfig.duration) : Promise.resolve()
);
Promise.all(promises).then(() => {
// Only remove if this is still the pending removal (not cancelled by a new toggle)
if (pendingRemoval === toRemove) {
removeNodes(toRemove);
pendingRemoval = null;
}
});
} else {
removeNodes(toRemove);
}
// Add new nodes (untracked so rendering doesn't subscribe this effect to state signals)
const renderFn = show ? thenFn : elseFn;
if (renderFn) {
const prev = currentEffect;
currentEffect = null; // Untrack: don't subscribe to signals during render
try {
const result = renderFn();
// Collect actual child nodes — DocumentFragments lose children when appended
let nodes;
if (result instanceof DocumentFragment) {
nodes = [...result.childNodes];
} else {
nodes = [].concat(result).flat().filter(n => n instanceof Node);
}
currentNodes = nodes.slice();
const frag = document.createDocumentFragment();
for (const n of nodes) frag.appendChild(n);
if (marker.parentNode) marker.parentNode.insertBefore(frag, marker.nextSibling);
if (animConfig && animConfig.enter) {
nodes.forEach(n => { if (n instanceof Element) animateIn(n, animConfig.enter, animConfig.duration, animConfig.delay); });
}
} finally {
currentEffect = prev;
}
}
});
}
// ─── List rendering ─────────────────────────────────
function listRender(parent, listFn, itemFn, animConfig) {
const marker = document.createComment("wf-for");
parent.appendChild(marker);
let currentNodes = [];
effect(() => {
const items = listFn(); // Track the list signal
// Remove old
if (animConfig && animConfig.exit && currentNodes.length) {
const toRemove = [...currentNodes];
toRemove.forEach((n, i) => {
if (n instanceof Element) {
animateOut(n, animConfig.exit, animConfig.duration).then(() => { if (n.parentNode) n.parentNode.removeChild(n); });
} else {
if (n.parentNode) n.parentNode.removeChild(n);
}
});
} else {
removeNodes(currentNodes);
}
currentNodes = [];
// Render items untracked
const prev = currentEffect;
currentEffect = null;
try {
const frag = document.createDocumentFragment();
if (items && items.length) {
items.forEach((item, index) => {
const result = itemFn(item, index);
let nodes;
if (result instanceof DocumentFragment) {
nodes = [...result.childNodes];
} else {
nodes = [].concat(result).flat().filter(n => n instanceof Node);
}
for (const n of nodes) {
frag.appendChild(n);
currentNodes.push(n);
if (animConfig && animConfig.enter && n instanceof Element) {
const delay = animConfig.stagger ? (parseInt(animConfig.stagger) * index) + "ms" : animConfig.delay;
animateIn(n, animConfig.enter, animConfig.duration, delay);
}
}
});
}
if (marker.parentNode) marker.parentNode.insertBefore(frag, marker.nextSibling);
} finally {
currentEffect = prev;
}
});
}
// ─── Show/Hide ───────────────────────────────────────
function showRender(parent, condFn, contentFn, animConfig) {
const wrapper = document.createElement("div");
wrapper.style.display = "contents";
const nodes = [].concat(contentFn()).flat();
for (const n of nodes) {
if (n instanceof Node) wrapper.appendChild(n);
}
parent.appendChild(wrapper);
if (animConfig) {
effect(() => {
if (condFn()) {
wrapper.style.display = "contents";
if (animConfig.enter) {
for (const n of wrapper.children) animateIn(n, animConfig.enter, animConfig.duration, animConfig.delay);
}
} else {
if (animConfig.exit) {
const promises = [...wrapper.children].map(n => animateOut(n, animConfig.exit, animConfig.duration));
Promise.all(promises).then(() => { wrapper.style.display = "none"; });
} else {
wrapper.style.display = "none";
}
}
});
} else {
effect(() => {
wrapper.style.display = condFn() ? "contents" : "none";
});
}
}
// ─── Router ──────────────────────────────────────────
let routerInstance = null;
// ─── Router ──────────────────────────────────────────
// Base path for deployment (set via WF.setBasePath or config)
let _basePath = "";
function _stripBase(fullPath) {
if (_basePath && fullPath.startsWith(_basePath)) {
const stripped = fullPath.slice(_basePath.length);
return stripped || "/";
}
return fullPath;
}
function createRouter(routes, container) {
// Check for SPA redirect from 404.html (?p=/path)
const urlParams = new URLSearchParams(window.location.search);
const redirectPath = urlParams.get("p");
if (redirectPath) {
window.history.replaceState(null, "", _basePath + redirectPath);
}
const initialPath = _stripBase(window.location.pathname);
const currentPath = signal(initialPath);
function matchRoute(path) {
for (const route of routes) {
const params = matchPath(route.path, path);
if (params !== null) return { route, params };
}
// Try wildcard
const wild = routes.find(r => r.path === "*");
if (wild) return { route: wild, params: {} };
return null;
}
function matchPath(pattern, path) {
if (pattern === path) return {};
const patternParts = pattern.split("/").filter(Boolean);
const pathParts = path.split("/").filter(Boolean);
if (patternParts.length !== pathParts.length) return null;
const params = {};
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(":")) {
params[patternParts[i].slice(1)] = pathParts[i];
} else if (patternParts[i] !== pathParts[i]) {
return null;
}
}
return params;
}
function render() {
const path = currentPath(); // Only subscribe to path changes
const match = matchRoute(path);
container.innerHTML = "";
if (match) {
// Untrack: don't subscribe the router effect to signals read during page render
const prev = currentEffect;
currentEffect = null;
try {
const el = match.route.render(match.params);
if (el instanceof Node) container.appendChild(el);
} finally {
currentEffect = prev;
}
}
}
window.addEventListener("popstate", () => {
currentPath.set(_stripBase(window.location.pathname));
});
effect(render);
routerInstance = {
navigate: (path) => {
window.history.pushState(null, "", _basePath + path);
currentPath.set(path);
},
currentPath,
back: () => window.history.back(),
forward: () => window.history.forward(),
};
return routerInstance;
}
let _ssgMode = false;
function setSsgMode(enabled) { _ssgMode = enabled; }
function setBasePath(path) { _basePath = path.replace(/\/$/, ""); }
function navigate(path) {
if (_ssgMode) {
// SSG: full page load to the pre-rendered HTML file
window.location.href = _basePath + path;
} else if (routerInstance) {
routerInstance.navigate(path);
} else {
window.location.href = path;
}
}
function getParams() {
return routerInstance ? routerInstance._currentParams || {} : {};
}
// ─── Store ───────────────────────────────────────────
function createStore(definition) {
const store = {};
const states = {};
// Create signals for each state
if (definition.state) {
for (const [key, val] of Object.entries(definition.state)) {
const s = signal(typeof val === "function" ? val() : val);
states[key] = s;
Object.defineProperty(store, key, {
get: () => s(),
set: (v) => s.set(v),
});
}
}
// Create computed for derived
if (definition.derived) {
for (const [key, fn] of Object.entries(definition.derived)) {
const c = computed(() => fn(store));
Object.defineProperty(store, key, { get: () => c() });
}
}
// Bind actions
if (definition.actions) {
for (const [key, fn] of Object.entries(definition.actions)) {
store[key] = (...args) => fn(store, ...args);
}
}
return store;
}
// ─── Fetch ───────────────────────────────────────────
function wfFetch(url, options, callbacks) {
const container = document.createDocumentFragment();
const wrapper = document.createElement("div");
wrapper.style.display = "contents";
const loading = signal(true);
const error = signal(null);
const data = signal(null);
// Show loading
if (callbacks.loading) {
const loadingEl = document.createElement("div");
loadingEl.style.display = "contents";
const nodes = [].concat(callbacks.loading()).flat();
for (const n of nodes) { if (n instanceof Node) loadingEl.appendChild(n); }
wrapper.appendChild(loadingEl);
effect(() => { loadingEl.style.display = loading() ? "contents" : "none"; });
}
// Success container
const successEl = document.createElement("div");
successEl.style.display = "contents";
wrapper.appendChild(successEl);
// Error container
const errorEl = document.createElement("div");
errorEl.style.display = "contents";
wrapper.appendChild(errorEl);
const resolvedUrl = typeof url === "function" ? url() : url;
const doFetch = () => {
const fetchUrl = typeof url === "function" ? url() : url;
loading.set(true);
error.set(null);
const fetchOpts = {};
if (options) {
if (options.method) fetchOpts.method = options.method;
if (options.headers) fetchOpts.headers = options.headers;
if (options.body) {
fetchOpts.body = JSON.stringify(typeof options.body === "function" ? options.body() : options.body);
fetchOpts.headers = { "Content-Type": "application/json", ...(fetchOpts.headers || {}) };
}
}
fetch(fetchUrl, fetchOpts)
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
.then(d => {
data.set(d);
loading.set(false);
if (callbacks.success) {
successEl.innerHTML = "";
const nodes = [].concat(callbacks.success(d)).flat();
for (const n of nodes) { if (n instanceof Node) successEl.appendChild(n); }
}
})
.catch(e => {
error.set(e);
loading.set(false);
if (callbacks.error) {
errorEl.innerHTML = "";
const nodes = [].concat(callbacks.error(e)).flat();
for (const n of nodes) { if (n instanceof Node) errorEl.appendChild(n); }
}
});
};
doFetch();
return wrapper;
}
// ─── Toast ───────────────────────────────────────────
let toastContainer = null;
function showToast(message, variant, duration) {
if (!toastContainer) {
toastContainer = document.createElement("div");
toastContainer.className = "wf-toast-container";
document.body.appendChild(toastContainer);
}
const toast = document.createElement("div");
toast.className = `wf-toast wf-toast--${variant || "info"}`;
toast.textContent = message;
toastContainer.appendChild(toast);
setTimeout(() => { toast.classList.add("wf-toast--exit"); setTimeout(() => toast.remove(), 300); }, duration || 3000);
}
// ─── Mount ───────────────────────────────────────────
function mount(renderFn, container) {
const el = renderFn();
if (el instanceof Node) {
container.innerHTML = "";
container.appendChild(el);
}
}
// ─── Hydrate (SSG) ─────────────────────────────────
function hydrate(renderFn, container) {
// If container already has pre-rendered content, keep it and
// run the render function to initialize signals, effects, and events.
// The render function builds DOM nodes that won't be inserted —
// instead, the existing DOM is kept and JS takes over.
if (container.children.length > 0) {
// Run render to initialize all signals and effects
renderFn();
// The effects will find and update the existing DOM nodes
} else {
// No pre-rendered content — fall back to full mount
mount(renderFn, container);
}
}
// ─── i18n ────────────────────────────────────────────
const RTL_LOCALES = new Set(["ar", "he", "fa", "ur"]);
let i18nInstance = null;
function createI18n(defaultLocale, translations) {
const locale = signal(defaultLocale);
const dir = signal(RTL_LOCALES.has(defaultLocale) ? "rtl" : "ltr");
function t(key, params) {
const currentLocale = locale();
const messages = translations[currentLocale] || translations[defaultLocale] || {};
let text = messages[key];
// Fallback to default locale
if (text === undefined && currentLocale !== defaultLocale) {
const fallback = translations[defaultLocale] || {};
text = fallback[key];
}
// Fallback to key itself
if (text === undefined) return key;
// Interpolate {placeholder} tokens
if (params && text.includes("{")) {
for (const [k, v] of Object.entries(params)) {
text = text.replace(new RegExp("\\{" + k + "\\}", "g"), String(v));
}
}
return text;
}
function setLocale(newLocale) {
locale.set(newLocale);
const newDir = RTL_LOCALES.has(newLocale) ? "rtl" : "ltr";
dir.set(newDir);
document.documentElement.setAttribute("lang", newLocale);
document.documentElement.setAttribute("dir", newDir);
}
i18nInstance = { t, locale, dir, setLocale };
return i18nInstance;
}
// ─── Icon System ────────────────────────────────────
// Built-in SVG icons for common UI needs
const _ICONS = {
close: '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
menu: '<path d="M3 12h18M3 6h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
search: '<circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2"/><path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
home: '<path d="M3 12l9-9 9 9M5 10v10a1 1 0 001 1h3v-5h6v5h3a1 1 0 001-1V10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
settings: '<circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 11-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 110-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 114 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 110 4h-.09a1.65 1.65 0 00-1.51 1z" fill="none" stroke="currentColor" stroke-width="2"/>',
check: '<polyline points="20 6 9 17 4 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
"chevron-down": '<polyline points="6 9 12 15 18 9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
"chevron-right": '<polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
"chevron-left": '<polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
plus: '<line x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
minus: '<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
edit: '<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" fill="none" stroke="currentColor" stroke-width="2"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" fill="none" stroke="currentColor" stroke-width="2"/>',
trash: '<polyline points="3 6 5 6 21 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>',
heart: '<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" fill="none" stroke="currentColor" stroke-width="2"/>',
mail: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" fill="none" stroke="currentColor" stroke-width="2"/><polyline points="22,6 12,13 2,6" fill="none" stroke="currentColor" stroke-width="2"/>',
bell: '<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 01-3.46 0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
download: '<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
upload: '<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
eye: '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2"/>',
link: '<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
calendar: '<rect x="3" y="4" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/><line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/><line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/><line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/>',
filter: '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>',
info: '<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><line x1="12" y1="16" x2="12" y2="12" stroke="currentColor" stroke-width="2"/><line x1="12" y1="8" x2="12.01" y2="8" stroke="currentColor" stroke-width="2"/>',
warning: '<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" fill="none" stroke="currentColor" stroke-width="2"/><line x1="12" y1="9" x2="12" y2="13" stroke="currentColor" stroke-width="2"/><line x1="12" y1="17" x2="12.01" y2="17" stroke="currentColor" stroke-width="2"/>',
"arrow-left": '<line x1="19" y1="12" x2="5" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="12 19 5 12 12 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
"arrow-right": '<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="12 5 19 12 12 19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
logout: '<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
copy: '<rect x="9" y="9" width="13" height="13" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" fill="none" stroke="currentColor" stroke-width="2"/>',
};
function _renderIcon(el, name) {
const svgData = _ICONS[name];
if (svgData) {
el.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none">${svgData}</svg>`;
} else {
// Fallback: render name as text
el.textContent = name;
}
}
// ─── Exports ─────────────────────────────────────────
return {
signal, effect, computed,
h, text, reactiveText, appendChildren,
condRender, listRender, showRender,
animateIn, animateOut, animateEl, replayAnimation,
createRouter, navigate, getParams,
createStore,
createI18n,
wfFetch, showToast,
mount, hydrate, setSsgMode, setBasePath,
get _basePath() { return _basePath; },
i18n: null,
};
})();