This commit is contained in:
2026-03-26 14:06:49 +01:00
parent adfe628dfd
commit 876874c2f0
51 changed files with 535 additions and 967 deletions

View File

@@ -1,5 +1,5 @@
/**
* SigPro Core
* SigPro Core
*/
(() => {
let activeEffect = null;
@@ -42,14 +42,11 @@
const $ = (initial, key = null) => {
if (typeof initial === "function") {
const subs = new Set();
let cached;
let dirty = true;
let cached, dirty = true;
const effect = () => {
if (effect._deleted) return;
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
const prev = activeEffect;
activeEffect = effect;
try {
@@ -59,57 +56,34 @@
dirty = false;
trigger(subs);
}
} finally {
activeEffect = prev;
}
} finally { activeEffect = prev; }
};
effect._deps = new Set();
effect._isComputed = true;
effect._subs = subs;
effect._deleted = false;
effect.markDirty = () => (dirty = true);
effect.stop = () => {
effect._deleted = true;
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
subs.clear();
};
if (currentOwner) currentOwner.cleanups.add(effect.stop);
return () => {
if (dirty) effect();
track(subs);
return cached;
};
return () => { if (dirty) effect(); track(subs); return cached; };
}
let value = initial;
if (key) {
const saved = localStorage.getItem(key);
if (saved !== null) {
try {
value = JSON.parse(saved);
} catch {
value = saved;
}
}
if (saved !== null) try { value = JSON.parse(saved); } catch { value = saved; }
}
const subs = new Set();
return (...args) => {
if (args.length) {
const next = typeof args[0] === "function" ? args[0](value) : args[0];
if (!Object.is(value, next)) {
value = next;
if (key) {
localStorage.setItem(key, JSON.stringify(value));
}
if (key) localStorage.setItem(key, JSON.stringify(value));
trigger(subs);
}
}
@@ -118,52 +92,57 @@
};
};
$.effect = (fn) => {
const owner = currentOwner;
$.watch = (target, fn) => {
const isExplicit = Array.isArray(target);
const callback = isExplicit ? fn : target;
const depsInput = isExplicit ? target : null;
const effect = () => {
if (effect._deleted) return;
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
effect._cleanups.forEach((c) => c());
effect._cleanups.clear();
if (typeof callback !== "function") return () => {};
const owner = currentOwner;
const runner = () => {
if (runner._deleted) return;
runner._deps.forEach((s) => s.delete(runner));
runner._deps.clear();
runner._cleanups.forEach((c) => c());
runner._cleanups.clear();
const prevEffect = activeEffect;
const prevOwner = currentOwner;
activeEffect = effect;
currentOwner = { cleanups: effect._cleanups };
effect.depth = prevEffect ? prevEffect.depth + 1 : 0;
activeEffect = runner;
currentOwner = { cleanups: runner._cleanups };
runner.depth = prevEffect ? prevEffect.depth + 1 : 0;
try {
fn();
if (isExplicit) {
activeEffect = null;
callback();
activeEffect = runner;
depsInput.forEach(d => typeof d === "function" && d());
} else {
callback();
}
} finally {
activeEffect = prevEffect;
currentOwner = prevOwner;
}
};
effect._deps = new Set();
effect._cleanups = new Set();
effect._deleted = false;
effect.stop = () => {
if (effect._deleted) return;
effect._deleted = true;
effectQueue.delete(effect);
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
effect._cleanups.forEach((c) => c());
effect._cleanups.clear();
if (owner) {
owner.cleanups.delete(effect.stop);
}
runner._deps = new Set();
runner._cleanups = new Set();
runner._deleted = false;
runner.stop = () => {
if (runner._deleted) return;
runner._deleted = true;
effectQueue.delete(runner);
runner._deps.forEach((s) => s.delete(runner));
runner._cleanups.forEach((c) => c());
if (owner) owner.cleanups.delete(runner.stop);
};
if (owner) owner.cleanups.add(effect.stop);
effect();
return effect.stop;
if (owner) owner.cleanups.add(runner.stop);
runner();
return runner.stop;
};
const sweep = (node) => {
@@ -174,7 +153,7 @@
node.childNodes?.forEach(sweep);
};
$.view = (fn) => {
const _view = (fn) => {
const cleanups = new Set();
const prev = currentOwner;
const container = document.createElement("div");
@@ -191,9 +170,7 @@
else container.appendChild(n instanceof Node ? n : document.createTextNode(String(n)));
};
process(res);
} finally {
currentOwner = prev;
}
} finally { currentOwner = prev; }
return {
_isRuntime: true,
container,
@@ -207,8 +184,7 @@
$.html = (tag, props = {}, content = []) => {
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
content = props;
props = {};
content = props; props = {};
}
const el = document.createElement(tag);
el._cleanups = new Set();
@@ -216,43 +192,27 @@
for (let [k, v] of Object.entries(props)) {
if (k.startsWith("on")) {
const name = k.slice(2).toLowerCase().split(".")[0];
const mods = k.slice(2).toLowerCase().split(".").slice(1);
const handler = (e) => {
if (mods.includes("prevent")) e.preventDefault();
if (mods.includes("stop")) e.stopPropagation();
v(e);
};
el.addEventListener(name, handler, { once: mods.includes("once") });
const handler = (e) => v(e);
el.addEventListener(name, handler);
el._cleanups.add(() => el.removeEventListener(name, handler));
} else if (k.startsWith("$")) {
const attr = k.slice(1);
const stopAttr = $.effect(() => {
el._cleanups.add($.watch(() => {
const val = typeof v === "function" ? v() : v;
if (el[attr] === val) return;
if (attr === "value" || attr === "checked") el[attr] = val;
else if (typeof val === "boolean") el.toggleAttribute(attr, val);
else val == null ? el.removeAttribute(attr) : el.setAttribute(attr, val);
});
el._cleanups.add(stopAttr);
if (el[attr] !== val) el[attr] = val;
}));
if (typeof v === "function") {
const evt = attr === "checked" ? "change" : "input";
const handler = (e) => v(e.target[attr]);
el.addEventListener(evt, handler);
el._cleanups.add(() => el.removeEventListener(evt, handler));
el.addEventListener(evt, (e) => v(e.target[attr]));
}
} else if (typeof v === "function") {
const stopAttr = $.effect(() => {
el._cleanups.add($.watch(() => {
const val = v();
if (k === "class" || k === "className") el.className = val || "";
else if (typeof val === "boolean") el.toggleAttribute(k, val);
if (k === "class") el.className = val || "";
else val == null ? el.removeAttribute(k) : el.setAttribute(k, val);
});
el._cleanups.add(stopAttr);
}));
} else {
if (k === "class" || k === "className") el.className = v || "";
else if (typeof v === "boolean") el.toggleAttribute(k, v);
else v == null ? el.removeAttribute(k) : el.setAttribute(k, v);
el.setAttribute(k, v);
}
}
@@ -262,74 +222,91 @@
const marker = document.createTextNode("");
el.appendChild(marker);
let nodes = [];
const stopList = $.effect(() => {
el._cleanups.add($.watch(() => {
const res = c();
const next = (Array.isArray(res) ? res : [res]).map((i) =>
i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? ""),
i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? "")
);
nodes.forEach((n) => {
sweep(n);
n.remove();
});
nodes.forEach((n) => { sweep(n); n.remove(); });
next.forEach((n) => marker.parentNode?.insertBefore(n, marker));
nodes = next;
});
el._cleanups.add(stopList);
}));
} else el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ""));
};
append(content);
return el;
};
$.ignore = (fn) => {
const prev = activeEffect;
activeEffect = null;
try {
return fn();
} finally {
activeEffect = prev;
}
$.if = (condition, thenVal, otherwiseVal = null) => {
const marker = document.createTextNode("");
const container = $.html("div", { style: "display:contents" }, [marker]);
let current = null, last = null;
$.watch(() => {
const state = !!(typeof condition === "function" ? condition() : condition);
if (state !== last) {
last = state;
if (current) current.destroy();
const branch = state ? thenVal : otherwiseVal;
if (branch) {
current = _view(() => typeof branch === "function" ? branch() : branch);
container.insertBefore(current.container, marker);
}
}
});
return container;
};
$.for = (source, render, keyFn) => {
const marker = document.createTextNode("");
const container = $.html("div", { style: "display:contents" }, [marker]);
const cache = new Map();
$.watch(() => {
const items = (typeof source === "function" ? source() : source) || [];
const newKeys = new Set();
items.forEach((item, index) => {
const key = keyFn(item, index);
newKeys.add(key);
let run = cache.get(key);
if (!run) {
run = _view(() => render(item, index));
cache.set(key, run);
}
container.insertBefore(run.container, marker);
});
cache.forEach((run, key) => {
if (!newKeys.has(key)) { run.destroy(); cache.delete(key); }
});
});
return container;
};
$.router = (routes) => {
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
const outlet = Div({ class: "router-outlet" });
let current = null;
$.effect(() => {
const path = sPath();
$.watch([sPath], () => {
if (current) current.destroy();
outlet.innerHTML = "";
const parts = path.split("/").filter(Boolean);
const route =
routes.find((r) => {
const rp = r.path.split("/").filter(Boolean);
return rp.length === parts.length && rp.every((p, i) => p.startsWith(":") || p === parts[i]);
}) || routes.find((r) => r.path === "*");
const path = sPath();
const route = routes.find(r => {
const rp = r.path.split("/").filter(Boolean);
const pp = path.split("/").filter(Boolean);
return rp.length === pp.length && rp.every((p, i) => p.startsWith(":") || p === pp[i]);
}) || routes.find(r => r.path === "*");
if (route) {
const params = {};
route.path
.split("/")
.filter(Boolean)
.forEach((p, i) => {
if (p.startsWith(":")) params[p.slice(1)] = parts[i];
});
current = $.ignore(() =>
$.view(() => {
const res = route.component(params);
return typeof res === "function" ? res() : res;
}),
);
route.path.split("/").filter(Boolean).forEach((p, i) => {
if (p.startsWith(":")) params[p.slice(1)] = path.split("/").filter(Boolean)[i];
});
current = _view(() => {
const res = route.component(params);
return typeof res === "function" ? res() : res;
});
outlet.appendChild(current.container);
}
});
return outlet;
};
@@ -339,21 +316,17 @@
const el = typeof target === "string" ? document.querySelector(target) : target;
if (!el) return;
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
const instance = $.view(typeof component === "function" ? component : () => component);
const instance = _view(typeof component === "function" ? component : () => component);
el.replaceChildren(instance.container);
MOUNTED_NODES.set(el, instance);
return instance;
};
const tags =
`div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(
/\s+/,
);
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(/\s+/);
tags.forEach((t) => {
window[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $.html(t, p, c);
});
window.$ = $;
})();
export const { $ } = window;
export const { $ } = window;

File diff suppressed because one or more lines are too long