Update
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 7s

This commit is contained in:
2026-05-07 17:00:43 +02:00
parent 72d98f9aa1
commit 5de2378899
14 changed files with 116 additions and 646 deletions

25
dist/sigpro-ui.css vendored
View File

@@ -4775,31 +4775,6 @@
flex-direction: var(--tabs-direction);
}
}
.footer {
@layer daisyui.l1.l2.l3 {
display: grid;
width: 100%;
grid-auto-flow: row;
place-items: start;
column-gap: calc(0.25rem * 4);
row-gap: calc(0.25rem * 10);
font-size: 0.875rem;
line-height: 1.25rem;
& > * {
display: grid;
place-items: start;
gap: calc(0.25rem * 2);
}
&.footer-center {
grid-auto-flow: column dense;
place-items: center;
text-align: center;
& > * {
place-items: center;
}
}
}
}
.stat {
@layer daisyui.l1.l2.l3 {
display: inline-grid;

View File

@@ -1,8 +1,8 @@
// src/editor.js
import { $ as $2, isFunc as isFunc2, h as h2 } from "./sigpro.js";
import { $ as $2, isFunc as isFunc2, h as h2 } from "sigpro";
// src/sigpro-ui.js
import { $, watch, h, mount, when, each, isFunc } from "./sigpro.js";
import { $, watch, h, mount, when, each, isFunc } from "sigpro";
var val = (val2) => typeof val2 === "function" ? val2() : val2;
var cls = (...classes) => classes.filter(Boolean).join(" ").trim();
var currentLocale = $("en");

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
// src/sigpro-ui.js
import { $, watch, h, mount, when, each, isFunc } from "./sigpro.js";
import { $, watch, h, mount, when, each, isFunc } from "sigpro";
var val = (val2) => typeof val2 === "function" ? val2() : val2;
var getBy = (item, field = "label") => item && typeof item === "object" ? item[field] : item;
var cls = (...classes) => classes.filter(Boolean).join(" ").trim();

File diff suppressed because one or more lines are too long

181
dist/sigpro-ui.js vendored
View File

@@ -92,7 +92,7 @@ var spui = (() => {
val: () => val
});
// src/sigpro.js
// node_modules/sigpro/dist/sigpro.esm.js
var isFunc = (f) => typeof f === "function";
var isObj = (o) => o && typeof o === "object";
var isArr = Array.isArray;
@@ -108,7 +108,8 @@ var spui = (() => {
var XLINK_NS = "http://www.w3.org/1999/xlink";
var SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
var dispose = (eff) => {
if (!eff || eff._disposed) return;
if (!eff || eff._disposed)
return;
eff._disposed = true;
const stack = [eff];
while (stack.length) {
@@ -128,7 +129,8 @@ var spui = (() => {
}
};
var onUnmount = (fn) => {
if (activeOwner) (activeOwner._cleanups ||= /* @__PURE__ */ new Set()).add(fn);
if (activeOwner)
(activeOwner._cleanups ||= /* @__PURE__ */ new Set()).add(fn);
};
var untrack = (fn) => {
const p = activeEffect;
@@ -141,8 +143,10 @@ var spui = (() => {
};
var createEffect = (fn, isComputed = false) => {
const effect = () => {
if (effect._disposed) return;
if (effect._deps) effect._deps.forEach((s) => s.delete(effect));
if (effect._disposed)
return;
if (effect._deps)
effect._deps.forEach((s) => s.delete(effect));
if (effect._cleanups) {
effect._cleanups.forEach((c) => c());
effect._cleanups.clear();
@@ -165,15 +169,19 @@ var spui = (() => {
effect._depth = activeEffect ? activeEffect._depth + 1 : 0;
effect._mounts = [];
effect._parent = activeOwner;
if (activeOwner) (activeOwner._children ||= /* @__PURE__ */ new Set()).add(effect);
if (activeOwner)
(activeOwner._children ||= /* @__PURE__ */ new Set()).add(effect);
return effect;
};
var flush = () => {
if (isFlushing) return;
if (isFlushing)
return;
isFlushing = true;
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth);
effectQueue.clear();
for (const e of sorted) if (!e._disposed) e();
for (const e of sorted)
if (!e._disposed)
e();
isFlushing = false;
};
var trackUpdate = (subs, trigger = false) => {
@@ -183,16 +191,19 @@ var spui = (() => {
} else if (trigger && subs.size > 0) {
let hasQueue = false;
for (const e of subs) {
if (e === activeEffect || e._disposed) continue;
if (e === activeEffect || e._disposed)
continue;
if (e._isComputed) {
e._dirty = true;
if (e._subs) trackUpdate(e._subs, true);
if (e._subs)
trackUpdate(e._subs, true);
} else {
effectQueue.add(e);
hasQueue = true;
}
}
if (hasQueue && !isFlushing && batchDepth === 0) queueMicrotask(flush);
if (hasQueue && !isFlushing && batchDepth === 0)
queueMicrotask(flush);
}
};
var $ = (val2, key = null) => {
@@ -224,16 +235,18 @@ var spui = (() => {
computed._disposed = false;
return computed;
}
if (key) try {
val2 = JSON.parse(localStorage.getItem(key)) ?? val2;
} catch (e) {
}
if (key)
try {
val2 = JSON.parse(localStorage.getItem(key)) ?? val2;
} catch (e) {
}
return (...args) => {
if (args.length) {
const next = isFunc(args[0]) ? args[0](val2) : args[0];
if (!Object.is(val2, next)) {
val2 = next;
if (key) localStorage.setItem(key, JSON.stringify(val2));
if (key)
localStorage.setItem(key, JSON.stringify(val2));
trackUpdate(subs, true);
}
}
@@ -255,22 +268,27 @@ var spui = (() => {
return () => dispose(effect);
};
var cleanupNode = (node) => {
if (!node) return;
if (!node)
return;
if (node._cleanups) {
node._cleanups.forEach((fn) => fn());
node._cleanups.clear();
}
if (node._ownerEffect) dispose(node._ownerEffect);
if (node.childNodes) node.childNodes.forEach((n) => cleanupNode(n));
if (node._ownerEffect)
dispose(node._ownerEffect);
if (node.childNodes)
node.childNodes.forEach((n) => cleanupNode(n));
};
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
var DANGEROUS_URI_ATTRS = /* @__PURE__ */ new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
var validateAttr = (key, val2) => {
if (val2 == null || val2 === false) return null;
if (val2 == null || val2 === false)
return null;
if (isDangerousAttr(key)) {
const sVal = String(val2);
if (DANGEROUS_PROTOCOL.test(sVal)) return "#";
if (DANGEROUS_PROTOCOL.test(sVal))
return "#";
}
return val2;
};
@@ -290,7 +308,8 @@ var spui = (() => {
});
effect();
const result = effect._result;
if (result == null) return null;
if (result == null)
return null;
const node = result instanceof Node || isArr(result) && result.every((n) => n instanceof Node) ? result : doc.createTextNode(String(result));
const attach = (n) => {
if (isObj(n) && !n._isRuntime) {
@@ -325,11 +344,16 @@ var spui = (() => {
} else if (isFunc(v)) {
const effect = createEffect(() => {
const val2 = validateAttr(k, v());
if (k === "class") el.className = val2 || "";
else if (val2 == null) el.removeAttribute(k);
else if (k === "style" && typeof val2 === "string") el.setAttribute("style", val2);
else if (k in el && !isSVG) el[k] = val2;
else el.setAttribute(k, val2 === true ? "" : val2);
if (k === "class")
el.className = val2 || "";
else if (val2 == null)
el.removeAttribute(k);
else if (k === "style" && typeof val2 === "string")
el.setAttribute("style", val2);
else if (k in el && !isSVG)
el[k] = val2;
else
el.setAttribute(k, val2 === true ? "" : val2);
});
effect();
el._cleanups.add(() => dispose(effect));
@@ -341,14 +365,18 @@ var spui = (() => {
} else {
const val2 = validateAttr(k, v);
if (val2 != null) {
if (k === "style" && typeof val2 === "string") el.setAttribute("style", val2);
else if (k in el && !isSVG) el[k] = val2;
else el.setAttribute(k, val2 === true ? "" : val2);
if (k === "style" && typeof val2 === "string")
el.setAttribute("style", val2);
else if (k in el && !isSVG)
el[k] = val2;
else
el.setAttribute(k, val2 === true ? "" : val2);
}
}
}
const append = (c) => {
if (isArr(c)) return c.forEach(append);
if (isArr(c))
return c.forEach(append);
if (isFunc(c)) {
const anchor = doc.createTextNode("");
el.appendChild(anchor);
@@ -357,15 +385,20 @@ var spui = (() => {
const res = c();
const next = (isArr(res) ? res : [res]).map(ensureNode);
currentNodes.forEach((n) => {
if (n._isRuntime) n.destroy();
else cleanupNode(n);
if (n.parentNode) n.remove();
if (n._isRuntime)
n.destroy();
else
cleanupNode(n);
if (n.parentNode)
n.remove();
});
let ref = anchor;
for (let i = next.length - 1; i >= 0; i--) {
const node = next[i];
if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref);
if (node._mounts) node._mounts.forEach((fn) => fn());
if (node.parentNode !== ref.parentNode)
ref.parentNode?.insertBefore(node, ref);
if (node._mounts)
node._mounts.forEach((fn) => fn());
ref = node;
}
currentNodes = next;
@@ -376,7 +409,8 @@ var spui = (() => {
} else {
const node = ensureNode(c);
el.appendChild(node);
if (node._mounts) node._mounts.forEach((fn) => fn());
if (node._mounts)
node._mounts.forEach((fn) => fn());
}
};
append(children);
@@ -392,7 +426,8 @@ var spui = (() => {
activeOwner = { _cleanups: cleanups };
activeEffect = null;
const processResult = (result) => {
if (!result) return;
if (!result)
return;
if (result._isRuntime) {
cleanups.add(result.destroy);
container.appendChild(result.container);
@@ -422,20 +457,17 @@ var spui = (() => {
const anchor = doc.createTextNode("");
const root = h("div", { style: "display:contents" }, [anchor]);
let currentView = null;
watch(
() => !!(isFunc(cond) ? cond() : cond),
(show) => {
if (currentView) {
currentView.destroy();
currentView = null;
}
const content = show ? SIP : NOP;
if (content) {
currentView = render(() => isFunc(content) ? content() : content);
root.insertBefore(currentView.container, anchor);
}
watch(() => !!(isFunc(cond) ? cond() : cond), (show) => {
if (currentView) {
currentView.destroy();
currentView = null;
}
);
const content = show ? SIP : NOP;
if (content) {
currentView = render(() => isFunc(content) ? content() : content);
root.insertBefore(currentView.container, anchor);
}
});
onUnmount(() => currentView?.destroy());
return root;
};
@@ -451,8 +483,10 @@ var spui = (() => {
const item = newItems[i];
const key = keyField ? item?.[keyField] ?? i : item?.id ?? i;
let view = cache.get(key);
if (!view) view = render(() => itemFn(item, i));
else cache.delete(key);
if (!view)
view = render(() => itemFn(item, i));
else
cache.delete(key);
nextCache.set(key, view);
nextOrder.push(view);
}
@@ -461,49 +495,20 @@ var spui = (() => {
for (let i = nextOrder.length - 1; i >= 0; i--) {
const view = nextOrder[i];
const node = view.container;
if (node.nextSibling !== lastRef) root.insertBefore(node, lastRef);
if (node.nextSibling !== lastRef)
root.insertBefore(node, lastRef);
lastRef = node;
}
cache = nextCache;
});
return root;
};
var router = (routes) => {
const getHash = () => window.location.hash.slice(1) || "/";
const path = $(getHash());
const handler = () => path(getHash());
window.addEventListener("hashchange", handler);
onUnmount(() => window.removeEventListener("hashchange", handler));
const hook = h("div", { class: "router-hook" });
let currentView = null;
watch([path], () => {
const cur = path();
const route = routes.find((r) => {
const p1 = r.path.split("/").filter(Boolean);
const p2 = cur.split("/").filter(Boolean);
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
}) || routes.find((r) => r.path === "*");
if (route) {
currentView?.destroy();
const params = {};
route.path.split("/").filter(Boolean).forEach((p, i) => {
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
});
router.params(params);
currentView = render(() => isFunc(route.component) ? route.component(params) : route.component);
hook.replaceChildren(currentView.container);
}
});
return hook;
};
router.params = $({});
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
router.back = () => window.history.back();
router.path = () => window.location.hash.replace(/^#/, "") || "/";
var mount = (comp, target) => {
const t2 = typeof target === "string" ? doc.querySelector(target) : target;
if (!t2) return;
if (MOUNTED_NODES.has(t2)) MOUNTED_NODES.get(t2).destroy();
if (!t2)
return;
if (MOUNTED_NODES.has(t2))
MOUNTED_NODES.get(t2).destroy();
const inst = render(isFunc(comp) ? comp : () => comp);
t2.replaceChildren(inst.container);
MOUNTED_NODES.set(t2, inst);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -44,12 +44,12 @@
"clean": "rm -rf ./dist ./css/*.css ./docs/*.js ./docs/*.css",
"build:css": "tailwindcss -i ./src/sigpro-ui.css -o ./dist/sigpro-ui.css --content './src/**/*.js' && du -h ./dist/sigpro-ui.css",
"build:cssmin": "tailwindcss -i ./src/sigpro-ui.css -o ./dist/sigpro-ui.min.css --content './src/**/*.js' --minify && du -h ./dist/sigpro-ui.css",
"build:js:iife": "esbuild ./src/build_umd.js --bundle --outfile=./dist/sigpro-ui.js --format=iife --global-name=spui --external:./src/sigpro.js=window",
"build:js:iife:min": "esbuild ./src/build_umd.js --bundle --outfile=./dist/sigpro-ui.min.js --format=iife --global-name=spui --minify --external:./src/sigpro.js=window",
"build:js:esm": "bun build ./src/build_esm.js --bundle --outfile=./dist/sigpro-ui.esm.js --format=esm --external ./src/sigpro.js",
"build:js:esm:min": "bun build ./src/build_esm.js --bundle --outfile=./dist/sigpro-ui.esm.min.js --format=esm --minify --external ./src/sigpro.js",
"build:js:editor:esm": "bun build ./src/build_editor.js --bundle --outfile=./dist/sigpro-ui.editor.esm.js --format=esm --external ./src/sigpro.js",
"build:js:editor:esm:min": "bun build ./src/build_editor.js --bundle --outfile=./dist/sigpro-ui.editor.esm.min.js --format=esm --minify --external ./src/sigpro.js",
"build:js:iife": "esbuild ./src/build_umd.js --bundle --outfile=./dist/sigpro-ui.js --format=iife --global-name=spui --external:sigpro=window",
"build:js:iife:min": "esbuild ./src/build_umd.js --bundle --outfile=./dist/sigpro-ui.min.js --format=iife --global-name=spui --minify --external:sigpro=window",
"build:js:esm": "bun build ./src/build_esm.js --bundle --outfile=./dist/sigpro-ui.esm.js --format=esm --external sigpro",
"build:js:esm:min": "bun build ./src/build_esm.js --bundle --outfile=./dist/sigpro-ui.esm.min.js --format=esm --minify --external sigpro",
"build:js:editor:esm": "bun build ./src/build_editor.js --bundle --outfile=./dist/sigpro-ui.editor.esm.js --format=esm --external sigpro",
"build:js:editor:esm:min": "bun build ./src/build_editor.js --bundle --outfile=./dist/sigpro-ui.editor.esm.min.js --format=esm --minify --external sigpro",
"copy:docs": "cp dist/sigpro-ui.min.css dist/sigpro-ui.min.js docs/",
"build": "bun run clean && bun run build:css && bun run build:cssmin && bun run build:js:iife && bun run build:js:iife:min && bun run build:js:esm && bun run build:js:esm:min && bun run build:js:editor:esm && bun run build:js:editor:esm:min && bun run copy:docs",
"docs": "bun x serve docs"
@@ -61,5 +61,8 @@
"daisyui": "^5.5.19",
"esbuild": "^0.28.0",
"tailwindcss": "^4.2.4"
},
"dependencies": {
"sigpro": "git+http://gitea:3000/natxocc/sigpro"
}
}

View File

@@ -1,4 +1,4 @@
import { $, isFunc, h } from "./sigpro.js"
import { $, isFunc, h } from "sigpro"
import { val, cls } from "./sigpro-ui.js"
export const Editor = (p) => {

View File

@@ -1,4 +1,4 @@
import { $, watch, h, mount, when, each, isFunc } from "./sigpro.js"
import { $, watch, h, mount, when, each, isFunc } from "sigpro"
export const val = val => typeof val === "function" ? val() : val;
export const getBy = (item, field = 'label') => (item && typeof item === 'object') ? item[field] : item;
export const cls = (...classes) => classes.filter(Boolean).join(' ').trim();

View File

@@ -1,513 +0,0 @@
const isFunc = f => typeof f === "function"
const isObj = o => o && typeof o === "object"
const isArr = Array.isArray
const doc = typeof document !== "undefined" ? document : null
const ensureNode = n => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n)))
let activeEffect = null
let activeOwner = null
let isFlushing = false
let batchDepth = 0
const effectQueue = new Set()
const proxyCache = new WeakMap()
const ITER = Symbol('iter')
const MOUNTED_NODES = new WeakMap()
const SVG_NS = "http://www.w3.org/2000/svg"
const XLINK_NS = "http://www.w3.org/1999/xlink"
const SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","))
const dispose = eff => {
if (!eff || eff._disposed) return
eff._disposed = true
const stack = [eff]
while (stack.length) {
const e = stack.pop()
if (e._cleanups) {
e._cleanups.forEach(fn => fn())
e._cleanups.clear()
}
if (e._children) {
e._children.forEach(child => stack.push(child))
e._children.clear()
}
if (e._deps) {
e._deps.forEach(depSet => depSet.delete(e))
e._deps.clear()
}
}
}
const onUnmount = fn => {
if (activeOwner) (activeOwner._cleanups ||= new Set()).add(fn)
}
const untrack = fn => {
const p = activeEffect
activeEffect = null
try { return fn() } finally { activeEffect = p }
}
const createEffect = (fn, isComputed = false) => {
const effect = () => {
if (effect._disposed) return
if (effect._deps) effect._deps.forEach(s => s.delete(effect))
if (effect._cleanups) {
effect._cleanups.forEach(c => c())
effect._cleanups.clear()
}
const prevEffect = activeEffect
const prevOwner = activeOwner
activeEffect = activeOwner = effect
try {
return effect._result = fn()
} catch (e) {
console.error("[SigPro]", e)
} finally {
activeEffect = prevEffect
activeOwner = prevOwner
}
}
effect._deps = effect._cleanups = effect._children = null
effect._disposed = false
effect._isComputed = isComputed
effect._depth = activeEffect ? activeEffect._depth + 1 : 0
effect._mounts = []
effect._parent = activeOwner
if (activeOwner) (activeOwner._children ||= new Set()).add(effect)
return effect
}
const flush = () => {
if (isFlushing) return
isFlushing = true
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth)
effectQueue.clear()
for (const e of sorted) if (!e._disposed) e()
isFlushing = false
}
const batch = fn => {
batchDepth++
try {
return fn()
} finally {
batchDepth--
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) flush()
}
}
const trackUpdate = (subs, trigger = false) => {
if (!trigger && activeEffect && !activeEffect._disposed) {
subs.add(activeEffect);
(activeEffect._deps ||= new Set()).add(subs)
} else if (trigger && subs.size > 0) {
let hasQueue = false
for (const e of subs) {
if (e === activeEffect || e._disposed) continue
if (e._isComputed) {
e._dirty = true
if (e._subs) trackUpdate(e._subs, true)
} else {
effectQueue.add(e)
hasQueue = true
}
}
if (hasQueue && !isFlushing && batchDepth === 0) queueMicrotask(flush)
}
}
const $ = (val, key = null) => {
const subs = new Set()
if (isFunc(val)) {
let cache
const computed = () => {
if (computed._dirty) {
const prev = activeEffect
activeEffect = computed
try {
const next = val()
if (!Object.is(cache, next)) {
cache = next
trackUpdate(subs, true)
}
} finally {
activeEffect = prev
}
computed._dirty = false
}
trackUpdate(subs)
return cache
}
computed._isComputed = true
computed._subs = subs
computed._dirty = true
computed._deps = null
computed._disposed = false
return computed
}
if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val } catch (e) { }
return (...args) => {
if (args.length) {
const next = isFunc(args[0]) ? args[0](val) : args[0]
if (!Object.is(val, next)) {
val = next
if (key) localStorage.setItem(key, JSON.stringify(val))
trackUpdate(subs, true)
}
}
trackUpdate(subs)
return val
}
}
const $$ = (target) => {
if (!isObj(target)) return target
const cached = proxyCache.get(target)
if (cached) return cached
const subs = new Map()
const getSubs = (key) => {
let set = subs.get(key)
if (!set) subs.set(key, set = new Set())
return set
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (typeof key !== 'symbol') trackUpdate(getSubs(key))
return $$(Reflect.get(target, key, receiver))
},
set(target, key, value, receiver) {
const hadKey = Reflect.has(target, key)
const oldValue = Reflect.get(target, key, receiver)
const result = Reflect.set(target, key, value, receiver)
if (result && !Object.is(oldValue, value)) {
trackUpdate(getSubs(key), true)
if (!hadKey) trackUpdate(getSubs(ITER), true)
}
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
if (result) {
trackUpdate(getSubs(key), true)
trackUpdate(getSubs(ITER), true)
}
return result
},
ownKeys(target) {
trackUpdate(getSubs(ITER))
return Reflect.ownKeys(target)
}
})
proxyCache.set(target, proxy)
return proxy
}
const watch = (sources, cb) => {
if (cb === undefined) {
const effect = createEffect(sources)
effect()
return () => dispose(effect)
}
const effect = createEffect(() => {
const vals = isArr(sources) ? sources.map(s => s()) : sources()
untrack(() => cb(vals))
})
effect()
return () => dispose(effect)
}
const cleanupNode = (node) => {
if (!node) return;
if (node._cleanups) {
node._cleanups.forEach(fn => fn());
node._cleanups.clear();
}
if (node._ownerEffect) dispose(node._ownerEffect);
if (node.childNodes) node.childNodes.forEach(n => cleanupNode(n));
};
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
const validateAttr = (key, val) => {
if (val == null || val === false) return null
if (isDangerousAttr(key)) {
const sVal = String(val)
if (DANGEROUS_PROTOCOL.test(sVal)) return '#'
}
return val
}
const h = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) {
children = props
props = {}
}
if (isFunc(tag)) {
const effect = createEffect(() => {
const result = tag(props, {
children,
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
})
effect._result = result
return result
})
effect()
const result = effect._result
if (result == null) return null
const node = (result instanceof Node || (isArr(result) && result.every(n => n instanceof Node)))
? result
: doc.createTextNode(String(result))
const attach = n => {
if (isObj(n) && !n._isRuntime) {
n._mounts = effect._mounts || []
n._cleanups = effect._cleanups || new Set()
n._ownerEffect = effect
}
}
isArr(node) ? node.forEach(attach) : attach(node)
return node
}
const isSVG = SVG_TAGS.has(tag)
const el = isSVG ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag)
el._cleanups = new Set()
for (const k of Object.keys(props)) {
let v = props[k]
if (k === "ref") {
isFunc(v) ? v(el) : (v.current = el)
continue
}
if (isSVG && k.startsWith("xlink:")) {
const cleanVal = validateAttr(k.slice(6), v)
cleanVal == null
? el.removeAttributeNS(XLINK_NS, k.slice(6))
: el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal)
continue
}
if (k.startsWith("on")) {
const ev = k.slice(2).toLowerCase()
el.addEventListener(ev, v)
const off = () => el.removeEventListener(ev, v)
el._cleanups.add(off)
onUnmount(off)
} else if (isFunc(v)) {
const effect = createEffect(() => {
const val = validateAttr(k, v())
if (k === "class") el.className = val || ""
else if (val == null) el.removeAttribute(k)
else if (k === "style" && typeof val === "string") el.setAttribute("style", val)
else if (k in el && !isSVG) el[k] = val
else el.setAttribute(k, val === true ? "" : val)
})
effect()
el._cleanups.add(() => dispose(effect))
onUnmount(() => dispose(effect))
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
const evType = k === "checked" ? "change" : "input"
el.addEventListener(evType, ev => v(ev.target[k]))
}
} else {
const val = validateAttr(k, v)
if (val != null) {
if (k === "style" && typeof val === "string") el.setAttribute("style", val)
else if (k in el && !isSVG) el[k] = val
else el.setAttribute(k, val === true ? "" : val)
}
}
}
const append = c => {
if (isArr(c)) return c.forEach(append)
if (isFunc(c)) {
const anchor = doc.createTextNode("")
el.appendChild(anchor)
let currentNodes = []
const effect = createEffect(() => {
const res = c()
const next = (isArr(res) ? res : [res]).map(ensureNode)
currentNodes.forEach(n => {
if (n._isRuntime) n.destroy()
else cleanupNode(n)
if (n.parentNode) n.remove()
})
let ref = anchor
for (let i = next.length - 1; i >= 0; i--) {
const node = next[i]
if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref)
if (node._mounts) node._mounts.forEach(fn => fn())
ref = node
}
currentNodes = next
})
effect()
el._cleanups.add(() => dispose(effect))
onUnmount(() => dispose(effect))
} else {
const node = ensureNode(c)
el.appendChild(node)
if (node._mounts) node._mounts.forEach(fn => fn())
}
}
append(children)
return el
}
const render = renderFn => {
const cleanups = new Set()
const previousOwner = activeOwner
const previousEffect = activeEffect
const container = doc.createElement("div")
container.style.display = "contents"
container.setAttribute("role", "presentation")
activeOwner = { _cleanups: cleanups }
activeEffect = null
const processResult = result => {
if (!result) return
if (result._isRuntime) {
cleanups.add(result.destroy)
container.appendChild(result.container)
} else if (isArr(result)) {
result.forEach(processResult)
} else {
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)))
}
}
try {
processResult(renderFn({ onCleanup: fn => cleanups.add(fn) }))
} finally {
activeOwner = previousOwner
activeEffect = previousEffect
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach(fn => fn())
cleanupNode(container)
container.remove()
}
}
}
const when = (cond, SIP, NOP = null) => {
const anchor = doc.createTextNode("")
const root = h("div", { style: "display:contents" }, [anchor])
let currentView = null
watch(
() => !!(isFunc(cond) ? cond() : cond),
show => {
if (currentView) {
currentView.destroy()
currentView = null
}
const content = show ? SIP : NOP
if (content) {
currentView = render(() => isFunc(content) ? content() : content)
root.insertBefore(currentView.container, anchor)
}
}
)
onUnmount(() => currentView?.destroy())
return root
}
const each = (src, itemFn, keyField) => {
const anchor = doc.createTextNode("")
const root = h("div", { style: "display:contents" }, [anchor])
let cache = new Map()
watch(() => (isFunc(src) ? src() : src) || [], items => {
const nextCache = new Map()
const nextOrder = []
const newItems = items || []
for (let i = 0; i < newItems.length; i++) {
const item = newItems[i]
const key = keyField ? (item?.[keyField] ?? i) : (item?.id ?? i)
let view = cache.get(key)
if (!view) view = render(() => itemFn(item, i))
else cache.delete(key)
nextCache.set(key, view)
nextOrder.push(view)
}
cache.forEach(view => view.destroy())
let lastRef = anchor
for (let i = nextOrder.length - 1; i >= 0; i--) {
const view = nextOrder[i]
const node = view.container
if (node.nextSibling !== lastRef) root.insertBefore(node, lastRef)
lastRef = node
}
cache = nextCache
})
return root
}
const router = routes => {
const getHash = () => window.location.hash.slice(1) || "/"
const path = $(getHash())
const handler = () => path(getHash())
window.addEventListener("hashchange", handler)
onUnmount(() => window.removeEventListener("hashchange", handler))
const hook = h("div", { class: "router-hook" })
let currentView = null
watch([path], () => {
const cur = path()
const route = routes.find(r => {
const p1 = r.path.split("/").filter(Boolean)
const p2 = cur.split("/").filter(Boolean)
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i])
}) || routes.find(r => r.path === "*")
if (route) {
currentView?.destroy()
const params = {}
route.path.split("/").filter(Boolean).forEach((p, i) => {
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]
})
router.params(params)
currentView = render(() => isFunc(route.component) ? route.component(params) : route.component)
hook.replaceChildren(currentView.container)
}
})
return hook
}
router.params = $({})
router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
router.back = () => window.history.back()
router.path = () => window.location.hash.replace(/^#/, "") || "/"
const Fragment = (props) => props.children;
const mount = (comp, target) => {
const t = typeof target === "string" ? doc.querySelector(target) : target
if (!t) return
if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy()
const inst = render(isFunc(comp) ? comp : () => comp)
t.replaceChildren(inst.container)
MOUNTED_NODES.set(t, inst)
return inst
}
if (typeof window !== "undefined") {
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video"
.split(" ")
.forEach(tag => { window[tag] = (props, children) => h(tag, props, children) })
}
export { $, $$, watch, batch, h, Fragment, mount, when, each, router, onUnmount, isArr, isFunc, isObj }