From 59de8eb287f0157fcc381eee3e2e227457ab4895 Mon Sep 17 00:00:00 2001 From: natxocc Date: Sat, 21 Mar 2026 21:12:56 +0100 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=20src/sigpro/sigpro.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sigpro/sigpro.js | 171 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/sigpro/sigpro.js diff --git a/src/sigpro/sigpro.js b/src/sigpro/sigpro.js new file mode 100644 index 0000000..94b9702 --- /dev/null +++ b/src/sigpro/sigpro.js @@ -0,0 +1,171 @@ +/** + * SigPro 2.0 - Atomic Unified Reactive Engine + */ +(() => { + /** @type {Function|null} */ + let activeEffect = null; + + /** + * @typedef {Object} SigPro + * @property {function(string, Object=, any=): HTMLElement} html - Creates a reactive HTML element. Supports attributes, events, and reactive signals. + * @property {function((HTMLElement|function), (HTMLElement|string)=): void} mount - Clears the target and appends the root component to the DOM. + * @property {function(Array): HTMLElement} router - Initializes a hash-based router that swaps components reactively. + * @property {function(function, Object=): void} use - Extends SigPro functionality by injecting the $ object into a plugin function. + */ + + /** + * Creates a Signal, Computed, or Effect. + * - If a value is passed: Creates a Signal. + * - If a function is passed: Creates a Computed/Effect that auto-tracks dependencies. + * * @example + * const $count = $(0); // Signal + * const $double = $(() => $count() * 2); // Computed + * * @param {any} initial - The initial value or a reactive function. + * @returns {Function & SigPro} Reactive accessor/mutator with SigPro tools attached. + */ + const $ = (initial) => { + const subs = new Set(); + const signal = (...args) => { + if (args.length) { + const next = typeof args[0] === 'function' ? args[0](initial) : args[0]; + if (!Object.is(initial, next)) { + initial = next; + subs.forEach(s => s()); + } + } + if (activeEffect) subs.add(activeEffect); + return initial; + }; + + if (typeof initial === 'function') { + let cached; + const runner = () => { + const prev = activeEffect; + activeEffect = runner; + try { cached = initial(); } finally { activeEffect = prev; } + subs.forEach(s => s()); + }; + runner(); + return signal; + } + return signal; + }; + + /** + * Hyperscript engine to render HTML nodes. + * Handles attributes (class, id), events (onclick), and reactive props ($value, $textContent). + * * @param {string} tag - The HTML tag name (e.g., 'div', 'button'). + * @param {Object} [props] - Object containing attributes, events, or $reactive_props. + * @param {any} [content] - String, Node, Array of nodes, or reactive function. + * @returns {HTMLElement} A live DOM element. + */ + $.html = (tag, props = {}, content = []) => { + const el = document.createElement(tag); + if (typeof props !== 'object' || props instanceof Node || Array.isArray(props) || typeof props === 'function') { + content = props; + props = {}; + } + + for (let [key, val] of Object.entries(props)) { + if (key.startsWith('on')) { + el.addEventListener(key.toLowerCase().slice(2), val); + } else if (key.startsWith('$')) { + const attr = key.slice(1); + if ((attr === 'value' || attr === 'checked') && typeof val === 'function') { + const ev = attr === 'checked' ? 'change' : 'input'; + el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value)); + } + $(() => { + 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 el.setAttribute(attr, v ?? ''); + }); + } else el.setAttribute(key, val); + } + + const append = (c) => { + if (Array.isArray(c)) return c.forEach(append); + if (typeof c === 'function') { + const node = document.createTextNode(''); + $(() => { + const res = c(); + if (res instanceof Node) { + if (node.parentNode) node.replaceWith(res); + } else { + node.textContent = res ?? ''; + } + }); + return el.appendChild(node); + } + el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? '')); + }; + append(content); + return el; + }; + + /** + * Application mounter. + * @param {HTMLElement|Function} node - The root component or element to mount. + * @param {HTMLElement|string} [target] - The DOM element or selector where the app will be injected. + */ + $.mount = (node, target = document.body) => { + const el = typeof target === 'string' ? document.querySelector(target) : target; + if (el) { + el.innerHTML = ''; + el.appendChild(typeof node === 'function' ? node() : node); + } + }; + + /** + * Reactive Client-side Hash Router. + * Tracks window.location.hash and renders the matching component. + * * @param {Array<{path: string, component: Function|HTMLElement}>} routes - Array of route definitions. + * @returns {HTMLElement} A reactive container that updates on route change. + */ + $.router = (routes) => { + const sPath = $(window.location.hash.replace(/^#/, "") || "/"); + window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/")); + + return $.html('div', [ + () => { + const current = sPath(); + const route = routes.find(r => { + const rP = r.path.split('/').filter(Boolean); + const cP = current.split('/').filter(Boolean); + if (rP.length !== cP.length) return false; + return rP.every((part, i) => part.startsWith(':') || part === cP[i]); + }) || routes.find(r => r.path === "*"); + + let component = route + ? (typeof route.component === 'function' ? route.component() : route.component) + : $.html('h1', "404"); + + if (!component) return $.html('h1', "404"); + return component instanceof Node ? component : $.html('span', String(component)); + } + ]); + }; + + /** + * Programmatic navigation. + * @param {string} path - The path to navigate to (e.g., '/home'). + */ + $.router.go = (path) => { + window.location.hash = path.startsWith('/') ? path : `/${path}`; + }; + + /** + * SigPro Plugin System. + * @param {function(SigPro, Object): void} plugin - Function that receives the SigPro instance. + * @param {Object} [options] - Configuration for the plugin. + */ + $.use = (plugin, options = {}) => { + if (typeof plugin === 'function') plugin($, options); + }; + + const tags = ['div', 'span', 'p', 'button', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'label', 'section', 'nav', 'main', 'header', 'footer', 'input', 'form', 'img', 'select', 'option', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'canvas', 'video', 'audio']; + tags.forEach(t => window[t] = (p, c) => $.html(t, p, c)); + + window.$ = $; +})(); \ No newline at end of file