Update sigpro and UI

This commit is contained in:
2026-03-23 15:17:05 +01:00
parent 2e1617ebe2
commit 6479a559ef
2 changed files with 640 additions and 288 deletions

View File

@@ -3,80 +3,142 @@
*/
(() => {
let activeEffect = null;
let currentOwner = null;
const effectQueue = new Set();
let isFlushScheduled = false;
let flushCount = 0;
let isFlushing = false;
const MOUNTED_NODES = new WeakMap();
const flushQueue = () => {
isFlushScheduled = false;
flushCount++;
if (flushCount > 100) {
const flush = () => {
if (isFlushing) return;
isFlushing = true;
while (effectQueue.size > 0) {
const sorted = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
effectQueue.clear();
throw new Error("SigPro: Bucle infinito detectado");
for (const eff of sorted) if (!eff._deleted) eff();
}
const effects = Array.from(effectQueue);
effectQueue.clear();
effects.forEach(fn => fn());
queueMicrotask(() => flushCount = 0);
isFlushing = false;
};
const scheduleFlush = (s) => {
effectQueue.add(s);
if (!isFlushScheduled) {
isFlushScheduled = true;
queueMicrotask(flushQueue);
const track = subs => {
if (activeEffect && !activeEffect._deleted) {
subs.add(activeEffect);
activeEffect._deps.add(subs);
}
};
const trigger = (subs) => {
for (const eff of subs) {
if (eff === activeEffect || eff._deleted) continue;
if (eff._isComputed) {
eff.markDirty();
if (eff._subs) trigger(eff._subs);
} else {
effectQueue.add(eff);
}
}
if (!isFlushing) queueMicrotask(flush);
};
const isObj = v => v && typeof v === 'object' && !(v instanceof Node);
const PROXIES = new WeakMap();
const RAW_SUBS = new WeakMap();
const getPropSubs = (target, prop) => {
let props = RAW_SUBS.get(target);
if (!props) RAW_SUBS.set(target, (props = new Map()));
let subs = props.get(prop);
if (!subs) props.set(prop, (subs = new Set()));
return subs;
};
const $ = (initial, key) => {
const subs = new Set();
if (initial?.constructor === Object && !key) {
const store = {};
for (let k in initial) store[k] = $(initial[k]);
return store;
if (isObj(initial) && !key && typeof initial !== 'function') {
if (PROXIES.has(initial)) return PROXIES.get(initial);
const proxy = new Proxy(initial, {
get(t, p, r) {
track(getPropSubs(t, p));
const val = Reflect.get(t, p, r);
return isObj(val) ? $(val) : val;
},
set(t, p, v, r) {
const old = Reflect.get(t, p, r);
if (Object.is(old, v)) return true;
const res = Reflect.set(t, p, v, r);
trigger(getPropSubs(t, p));
if (Array.isArray(t) && p !== 'length') trigger(getPropSubs(t, 'length'));
return res;
},
deleteProperty(t, p) {
const res = Reflect.deleteProperty(t, p);
trigger(getPropSubs(t, p));
return res;
}
});
PROXIES.set(initial, proxy);
return proxy;
}
if (typeof initial === 'function') {
let cached, running = false;
const cleanups = new Set();
const subs = new Set();
let cached, dirty = true;
const runner = () => {
if (runner.el && !runner.el.isConnected) return;
if (running) return;
cleanups.forEach(fn => fn());
cleanups.clear();
const effect = () => {
if (effect._deleted) return;
effect._cleanups.forEach(c => c());
effect._cleanups.clear();
effect._deps.forEach(s => s.delete(effect));
effect._deps.clear();
const prev = activeEffect;
activeEffect = runner;
activeEffect.onCleanup = (fn) => cleanups.add(fn);
running = true;
activeEffect = effect;
try {
const next = initial();
if (!Object.is(cached, next)) {
cached = next;
subs.forEach(scheduleFlush);
let maxD = 0;
effect._deps.forEach(s => { if (s._d > maxD) maxD = s._d; });
effect.depth = maxD + 1;
subs._d = effect.depth;
const val = initial();
if (!Object.is(cached, val) || dirty) {
cached = val;
dirty = false;
trigger(subs);
}
} finally {
activeEffect = prev;
running = false;
}
};
runner();
effect._isComputed = true;
effect._deps = new Set();
effect._cleanups = new Set();
effect._subs = subs;
effect.markDirty = () => dirty = true;
effect.stop = () => {
effect._deleted = true;
effectQueue.delete(effect);
effect._cleanups.forEach(c => c());
effect._deps.forEach(s => s.delete(effect));
subs.clear();
};
if (currentOwner) {
currentOwner.cleanups.add(effect.stop);
effect._isComputed = false;
effect();
return () => { };
}
return () => {
if (activeEffect) subs.add(activeEffect);
if (dirty) effect();
track(subs);
return cached;
};
}
const subs = new Set();
subs._d = 0;
if (key) {
const saved = localStorage.getItem(key);
if (saved !== null) try { initial = JSON.parse(saved); } catch (e) { }
try { const s = localStorage.getItem(key); if (s !== null) initial = JSON.parse(s); } catch (e) { }
}
return (...args) => {
@@ -84,143 +146,147 @@
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
if (!Object.is(initial, next)) {
initial = next;
if (key) localStorage.setItem(key, JSON.stringify(initial));
subs.forEach(scheduleFlush);
if (key) try { localStorage.setItem(key, JSON.stringify(initial)); } catch (e) { }
trigger(subs);
}
}
if (activeEffect) {
subs.add(activeEffect);
if (activeEffect.onCleanup) activeEffect.onCleanup(() => subs.delete(activeEffect));
}
track(subs);
return initial;
};
};
const sweep = node => {
if (node._cleanups) { node._cleanups.forEach(f => f()); node._cleanups.clear(); }
node.childNodes?.forEach(sweep);
};
const createRuntime = fn => {
const cleanups = new Set();
const prev = currentOwner;
currentOwner = { cleanups };
const container = $.html('div', { style: 'display:contents' });
try {
const res = fn({ onCleanup: f => cleanups.add(f) });
const process = n => {
if (!n) return;
if (n._isRuntime) { cleanups.add(n.destroy); container.appendChild(n.container); }
else if (Array.isArray(n)) n.forEach(process);
else container.appendChild(n instanceof Node ? n : document.createTextNode(String(n)));
};
process(res);
} finally { currentOwner = prev; }
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach(f => f());
sweep(container);
container.remove();
}
};
};
$.html = (tag, props = {}, content = []) => {
const el = document.createElement(tag);
if (props instanceof Node || Array.isArray(props) || typeof props !== 'object') {
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
content = props; props = {};
}
const el = document.createElement(tag);
el._cleanups = new Set();
for (let [key, val] of Object.entries(props)) {
if (key.startsWith('on')) {
const [rawName, ...mods] = key.toLowerCase().slice(2).split('.');
const handler = (e) => {
if (mods.includes('prevent')) e.preventDefault();
if (mods.includes('stop')) e.stopPropagation();
if (mods.some(m => m.startsWith('debounce'))) {
const ms = mods.find(m => m.startsWith('debounce')).split(':')[1] || 300;
clearTimeout(val._timer);
val._timer = setTimeout(() => val(e), ms);
} else {
val(e);
}
};
el.addEventListener(rawName, handler, { once: mods.includes('once') });
}
else if (key.startsWith('$')) {
const attr = key.slice(1);
const attrEff = () => {
const v = typeof val === 'function' ? val() : val;
if (attr === 'value' || attr === 'checked') el[attr] = v;
else if (typeof v === 'boolean') el.toggleAttribute(attr, v);
else if (v == null) el.removeAttribute(attr);
else el.setAttribute(attr, v);
};
attrEff.el = el; $(attrEff);
if ((attr === 'value' || attr === 'checked') && typeof val === 'function') {
el.addEventListener(attr === 'checked' ? 'change' : 'input', e =>
val(attr === 'checked' ? e.target.checked : e.target.value)
);
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') });
el._cleanups.add(() => el.removeEventListener(name, handler));
} else if (k.startsWith('$')) {
const attr = k.slice(1);
const stopAttr = $(() => {
const val = typeof v === 'function' ? v() : v;
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 ((attr === 'value' || attr === 'checked') && typeof v === 'function') {
const evt = attr === 'checked' ? 'change' : 'input';
const h = e => v(e.target[attr]);
el.addEventListener(evt, h);
el._cleanups.add(() => el.removeEventListener(evt, h));
}
} else el.setAttribute(key, val);
} else el.setAttribute(k, v);
}
const append = (c) => {
if (Array.isArray(c)) return c.forEach(append);
if (typeof c === 'function') {
let nodes = [document.createTextNode('')];
const contentEff = () => {
const append = c => {
if (Array.isArray(c)) c.forEach(append);
else if (typeof c === 'function') {
const marker = document.createTextNode('');
el.appendChild(marker);
let nodes = [marker];
const stopList = $(() => {
const res = c();
const nextNodes = (Array.isArray(res) ? res : [res]).map(i =>
i instanceof Node ? i : document.createTextNode(i ?? '')
);
if (nextNodes.length === 0) nextNodes.push(document.createTextNode(''));
if (nodes[0].parentNode) {
const parent = nodes[0].parentNode;
nextNodes.forEach(n => parent.insertBefore(n, nodes[0]));
nodes.forEach(n => n.remove());
nodes = nextNodes;
const next = (Array.isArray(res) ? res : [res]).map(i => i?.container || (i instanceof Node ? i : document.createTextNode(i ?? '')));
if (marker.parentNode) {
next.forEach(n => marker.parentNode.insertBefore(n, marker));
nodes.forEach(n => { if (n !== marker) { sweep(n); n.remove(); } });
nodes = [...next, marker];
}
};
contentEff.el = nodes[0];
nodes.forEach(n => el.appendChild(n));
$(contentEff);
return;
}
el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ''));
});
el._cleanups.add(stopList);
} else el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ''));
};
append(content);
return el;
};
const tags = ['div', 'span', 'p', 'h1', 'h2', 'ul', 'li', 'button', 'input', 'label', 'form', 'section', 'a', 'img'];
const tags = ['div', 'span', 'p', 'h1', 'h2', 'h3', 'ul', 'li', 'button', 'input', 'label', 'form', 'section', 'a', 'img', 'nav', 'hr'];
window.$ = new Proxy($, { get: (t, p) => t[p] || (tags.includes(p) ? (pr, c) => t.html(p, pr, c) : undefined) });
tags.forEach(t => window[t] = (p, c) => $.html(t, p, c));
$.router = (routes) => {
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
$.router = routes => {
const sPath = $(window.location.hash.replace(/^#/, '') || '/');
const handler = () => sPath(window.location.hash.replace(/^#/, '') || '/');
window.addEventListener('hashchange', handler);
const outlet = $.html('div', { class: 'router-outlet' });
let current = null;
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
const container = div({ class: "router-outlet" });
const routeEff = () => {
const cur = sPath();
const cP = cur.split('/').filter(Boolean);
if (currentOwner) currentOwner.cleanups.add(() => {
window.removeEventListener('hashchange', handler);
if (current) current.destroy();
});
$(() => {
const path = sPath(), parts = path.split('/').filter(Boolean);
const route = routes.find(r => {
const rP = r.path.split('/').filter(Boolean);
return rP.length === cP.length && rP.every((p, i) => p.startsWith(':') || p === cP[i]);
}) || routes.find(r => r.path === "*");
if (!route) return container.replaceChildren(h1("404 - Not Found"));
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 === '*');
if (current) current.destroy();
if (!route) return outlet.replaceChildren($.html('h1', '404'));
const params = {};
route.path.split('/').filter(Boolean).forEach((p, i) => {
if (p.startsWith(':')) params[p.slice(1)] = cP[i];
});
const res = typeof route.component === 'function' ? route.component(params) : route.component;
if (res instanceof Promise) {
const loader = span("Cargando...");
container.replaceChildren(loader);
res.then(c => container.replaceChildren(c instanceof Node ? c : document.createTextNode(c)));
} else {
container.replaceChildren(res instanceof Node ? res : document.createTextNode(res));
}
};
routeEff.el = container;
$(routeEff);
return container;
route.path.split('/').filter(Boolean).forEach((p, i) => { if (p.startsWith(':')) params[p.slice(1)] = parts[i]; });
current = createRuntime(() => route.component(params));
outlet.replaceChildren(current.container);
});
return outlet;
};
$.router.go = (path) => {
const target = path.startsWith('/') ? path : `/${path}`;
window.location.hash = target;
};
$.router.go = p => window.location.hash = p.replace(/^#?\/?/, '#/');
$.mount = (node, target = 'body') => {
$.mount = (component, target) => {
const el = typeof target === 'string' ? document.querySelector(target) : target;
if (el) { el.innerHTML = ''; el.appendChild(typeof node === 'function' ? node() : node); }
if (!el) return;
if (MOUNTED_NODES.has(el)) {
MOUNTED_NODES.get(el).destroy();
}
const instance = createRuntime(typeof component === 'function' ? component : () => component);
el.replaceChildren(instance.container);
MOUNTED_NODES.set(el, instance);
return instance;
};
window.$ = $;
})();
export const { $ } = window;
})();