New modular Sigpro
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s

This commit is contained in:
2026-04-27 15:22:57 +02:00
parent b931434edc
commit 99780e8399
21 changed files with 888 additions and 1036 deletions

137
sigpro.js
View File

@@ -17,6 +17,9 @@ 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(","))
let attrFilter = null
const filterXSS = fn => { attrFilter = fn }
const dispose = eff => {
if (!eff || eff._disposed) return
eff._disposed = true
@@ -232,22 +235,6 @@ const cleanupNode = (node) => {
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)) {
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`)
return '#'
}
}
return val
}
const h = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || !isObj(props)) {
children = props
@@ -294,36 +281,37 @@ const h = (tag, props = {}, children = []) => {
isFunc(v) ? v(el) : (v.current = el)
continue
}
let val = attrFilter ? attrFilter(k, v) : v
if (isSVG && k.startsWith("xlink:")) {
const cleanVal = validateAttr(k.slice(6), v)
cleanVal == null
val == null
? el.removeAttributeNS(XLINK_NS, k.slice(6))
: el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal)
: el.setAttributeNS(XLINK_NS, k.slice(6), val)
continue
}
if (k.startsWith("on")) {
const ev = k.slice(2).toLowerCase()
el.addEventListener(ev, v)
const off = () => el.removeEventListener(ev, v)
el.addEventListener(ev, val)
const off = () => el.removeEventListener(ev, val)
el._cleanups.add(off)
onUnmount(off)
} else if (isFunc(v)) {
} else if (isFunc(val)) {
const effect = createEffect(() => {
const val = validateAttr(k, v())
if (k === "class") el.className = val || ""
else if (val == null) el.removeAttribute(k)
else if (k in el && !isSVG) el[k] = val
else el.setAttribute(k, val === true ? "" : val)
const raw = val()
const safeVal = attrFilter ? attrFilter(k, raw) : raw
if (k === "class") el.className = safeVal || ""
else if (safeVal == null) el.removeAttribute(k)
else if (k in el && !isSVG) el[k] = safeVal
else el.setAttribute(k, safeVal === true ? "" : safeVal)
})
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]))
el.addEventListener(evType, ev => val(ev.target[k]))
}
} else {
const val = validateAttr(k, v)
if (val != null) {
if (k in el && !isSVG) el[k] = val
else el.setAttribute(k, val === true ? "" : val)
@@ -432,36 +420,6 @@ const when = (cond, SIP, NOP = null) => {
return root
}
const fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
const el = typeof child === "function" ? child() : child;
if (!(el instanceof Node)) return el;
if (name) {
el.style.animation = `${name}-in ${duration}ms`;
return el;
}
const hasTransform = scale || slide || rotate || blur;
const initialTransform = [
scale ? "scale(0.95)" : "",
slide ? "translateY(-10px)" : "",
rotate ? "rotate(-2deg)" : ""
].filter(Boolean).join(" ");
el.style.transition = `all ${duration}ms ease`;
el.style.opacity = "0";
if (hasTransform) el.style.transform = initialTransform;
if (blur) el.style.filter = "blur(4px)";
requestAnimationFrame(() => {
el.style.opacity = "1";
if (hasTransform) el.style.transform = "none";
if (blur) el.style.filter = "none";
});
return el;
};
const each = (src, itemFn, keyField) => {
const anchor = doc.createTextNode("")
const root = h("div", { style: "display:contents" }, [anchor])
@@ -525,52 +483,6 @@ router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
router.back = () => window.history.back()
router.path = () => window.location.hash.replace(/^#/, "") || "/"
const req = ({ url, method = 'GET', headers = {} }) => {
const loading = $(false);
const error = $(null);
const data = $(null);
let controller = null;
let timeoutId = null;
const run = async (body = null) => {
controller?.abort();
clearTimeout(timeoutId);
controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), 10000);
loading(true);
error(null);
try {
const isFormData = body instanceof FormData;
const res = await fetch(url, {
method,
headers: isFormData ? headers : { 'Content-Type': 'application/json', ...headers },
body: isFormData ? body : (body ? JSON.stringify(body) : undefined),
signal: controller.signal
});
const text = await res.text();
const json = text ? JSON.parse(text) : null;
if (!res.ok) throw new Error(json?.message || res.statusText);
data(json);
return json;
} catch (e) {
if (e.name !== 'AbortError') error(e.message);
throw e;
} finally {
loading(false);
clearTimeout(timeoutId);
controller = null;
timeoutId = null;
}
};
const abort = () => controller?.abort();
return { run, abort, loading, error, data };
};
const mount = (comp, target) => {
const t = typeof target === "string" ? doc.querySelector(target) : target
if (!t) return
@@ -581,17 +493,4 @@ const mount = (comp, target) => {
return inst
}
const sigproFn = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch })
const sigpro = () => {
if (typeof window !== "undefined") {
Object.assign(window, sigproFn)
"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) })
console.log("SigPro DX installed.")
}
}
if (typeof import.meta === 'undefined' && typeof window !== 'undefined') sigpro()
export { sigpro, $, $$, watch, h, when, each, fx, router, req, mount, batch }
export { $, $$, watch, h, when, each, router, mount, batch, filterXSS }