From 443315aae488696ad03f6cfa5ae038bb22fef491 Mon Sep 17 00:00:00 2001 From: natxocc Date: Sat, 21 Mar 2026 21:27:50 +0100 Subject: [PATCH] update --- src/plugins/debug.js | 41 ++++++ src/plugins/fetch.js | 26 ++-- src/plugins/ui.js | 311 ++++++++++++++++++++++++++----------------- src/sigpro/sigpro.js | 125 ++++++++++------- 4 files changed, 323 insertions(+), 180 deletions(-) create mode 100644 src/plugins/debug.js diff --git a/src/plugins/debug.js b/src/plugins/debug.js new file mode 100644 index 0000000..a9567ae --- /dev/null +++ b/src/plugins/debug.js @@ -0,0 +1,41 @@ +/** + * SigPro Debug Plugin + * Reactive state logger for signals and computed values. + */ +$.plugin(($) => { + /** + * Tracks a signal and logs every state change to the browser console. + * @param {Function} $sig - The reactive signal or computed function to monitor. + * @param {string} [name="Signal"] - A custom label to identify the log entry. + * @example + * const $count = $(0); + * $.debug($count, "Counter"); + * $count(1); // Logs: Counter | Old: 0 | New: 1 + */ + $.debug = ($sig, name = "Signal") => { + if (typeof $sig !== 'function') { + return console.warn(`[SigPro Debug] Cannot track "${name}": Not a function/signal.`); + } + + let prev = $sig(); + + $(() => { + const next = $sig(); + + if (Object.is(prev, next)) return; + + console.group(`%c SigPro Debug: ${name} `, "background: #1a1a1a; color: #bada55; font-weight: bold; border-radius: 3px; padding: 2px;"); + + console.log("%c Previous Value:", "color: #ff6b6b; font-weight: bold;", prev); + console.log("%c Current Value: ", "color: #51cf66; font-weight: bold;", next); + + if (next && typeof next === 'object') { + console.table(next); + } + + console.groupEnd(); + + prev = next; + }); + }; +}); \ No newline at end of file diff --git a/src/plugins/fetch.js b/src/plugins/fetch.js index 9fdd9dd..724087f 100644 --- a/src/plugins/fetch.js +++ b/src/plugins/fetch.js @@ -1,13 +1,20 @@ /** * SigPro Fetch Plugin - * Adds reactive data fetching to the $ namespace. + * Adds reactive data fetching capabilities to the SigPro instance. */ -$.use(($) => { +$.plugin(($) => { /** - * Performs a reactive fetch request. - * @param {string} url - The API endpoint. - * @param {Object} [options] - Fetch options (method, headers, etc). - * @returns {{ $data: Function, $loading: Function, $error: Function }} + * Performs a reactive asynchronous fetch request. + * @param {string} url - The URL of the resource to fetch. + * @param {RequestInit} [options] - Optional settings for the fetch request (method, headers, body, etc.). + * @returns {{ $data: Function, $loading: Function, $error: Function }} + * An object containing reactive signals for the response data, loading state, and error message. + * * @example + * const { $data, $loading, $error } = $.fetch('https://api.example.com/users'); + * return div([ + * () => $loading() ? "Loading..." : ul($data().map(user => li(user.name))), + * () => $error() && span({ class: 'text-red' }, $error()) + * ]); */ $.fetch = (url, options = {}) => { const $data = $(null); @@ -16,11 +23,14 @@ $.use(($) => { fetch(url, options) .then(res => { - if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); + if (!res.ok) throw new Error(`HTTP error! Status: ${res.status}`); return res.json(); }) .then(json => $data(json)) - .catch(err => $error(err.message)) + .catch(err => { + console.error("[SigPro Fetch Error]:", err); + $error(err.message); + }) .finally(() => $loading(false)); return { $data, $loading, $error }; diff --git a/src/plugins/ui.js b/src/plugins/ui.js index 0163a8b..98cddf0 100644 --- a/src/plugins/ui.js +++ b/src/plugins/ui.js @@ -1,141 +1,206 @@ /** * SigPro UI 2.0 - daisyUI v5 & Tailwind v4 Plugin + * Provides a set of reactive functional components. */ -(() => { - if (!window.$) return console.error("[SigPro UI] Fatal: SigPro 2.0 Core not found."); +$.plugin(($) => { + const ui = {}; - $.use(($) => { - const ui = {}; + /** + * Internal helper to merge base classes with reactive or static extra classes. + * @param {string} base - The default daisyUI class. + * @param {string|function} extra - User-provided classes. + * @returns {string|function} Merged classes. + */ + const parseClass = (base, extra) => { + if (typeof extra === 'function') return () => `${base} ${extra() || ''}`; + return `${base} ${extra || ''}`; + }; - const parseClass = (base, extra) => { - if (typeof extra === 'function') return () => `${base} ${extra() || ''}`; - return `${base} ${extra || ''}`; - }; + /** + * Standard Button component. + * @param {Object} p - Properties. + * @param {string|function} [p.class] - Extra CSS classes. + * @param {function} [p.$loading] - Reactive loading state. + * @param {function} [p.$disabled] - Reactive disabled state. + * @param {HTMLElement|string} [p.icon] - Leading icon. + * @param {string} [p.badge] - Badge text. + * @param {any} c - Children content. + */ + ui._button = (p, c) => button({ + ...p, + class: parseClass('btn', p.$class || p.class), + $disabled: () => p.$disabled?.() || p.$loading?.() + }, [ + () => p.$loading?.() ? span({ class: 'loading loading-spinner' }) : null, + p.icon && span({ class: 'mr-1' }, p.icon), + c, + p.badge && span({ class: `badge ${p.badgeClass || ''}` }, p.badge) + ]); - /** _button @param {Object} p @param {any} [c] */ - ui._button = (p, c) => button({ - ...p, - class: parseClass('btn', p.$class || p.class), - $disabled: () => p.$disabled?.() || p.$loading?.() - }, [ - () => p.$loading?.() ? span({ class: 'loading loading-spinner' }) : null, - p.icon && span({ class: 'mr-1' }, p.icon), + /** + * Form Input with label, tooltip, and error handling. + * @param {Object} p - Input properties. + * @param {string} [p.label] - Field label. + * @param {string} [p.tip] - Tooltip text. + * @param {function} [p.$value] - Reactive signal for the value. + * @param {function} [p.$error] - Reactive signal for error messages. + */ + ui._input = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ + p.label && div({ class: 'flex items-center gap-2' }, [ + span(p.label), + p.tip && div({ class: 'tooltip tooltip-right', 'data-tip': p.tip }, + span({ class: 'badge badge-ghost badge-xs' }, '?')) + ]), + $.html('input', { + ...p, + class: parseClass('input input-bordered w-full', p.$class || p.class), + $value: p.$value + }), + () => p.$error?.() ? span({ class: 'text-error text-xs' }, p.$error()) : null + ]); + + /** + * Select dropdown component. + * @param {Object} p - Select properties. + * @param {Array<{value: any, label: string}>} p.options - Array of options. + * @param {function} p.$value - Reactive signal for the selected value. + */ + ui._select = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ + p.label && span(p.label), + select({ + ...p, + class: parseClass('select select-bordered', p.$class || p.class), + onchange: (e) => p.$value?.(e.target.value) + }, (p.options || []).map(o => + $.html('option', { value: o.value, selected: o.value === p.$value?.() }, o.label)) + ) + ]); + + /** + * Checkbox component. + */ + ui._checkbox = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ + $.html('input', { type: 'checkbox', ...p, class: parseClass('checkbox', p.$class || p.class), $checked: p.$value }), + p.label && span({ class: 'label-text' }, p.label) + ]); + + /** + * Radio button component. + */ + ui._radio = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ + $.html('input', { + type: 'radio', ...p, + class: parseClass('radio', p.$class || p.class), + $checked: () => p.$value?.() === p.value, + onclick: () => p.$value?.(p.value) + }), + p.label && span({ class: 'label-text' }, p.label) + ]); + + /** + * Range slider component. + */ + ui._range = (p) => div({ class: 'flex flex-col gap-2' }, [ + p.label && span({ class: 'label-text' }, p.label), + $.html('input', { type: 'range', ...p, class: parseClass('range', p.$class || p.class), $value: p.$value }) + ]); + + /** + * Modal dialog component. + * @param {Object} p - Modal properties. + * @param {function} p.$open - Reactive signal (boolean) to control visibility. + * @param {any} c - Modal body content. + */ + ui._modal = (p, c) => () => p.$open() ? dialog({ class: 'modal modal-open' }, [ + div({ class: 'modal-box' }, [ + p.title && h3({ class: 'text-lg font-bold mb-4' }, p.title), c, - p.badge && span({ class: `badge ${p.badgeClass || ''}` }, p.badge) - ]); + div({ class: 'modal-action' }, ui._button({ onclick: () => p.$open(false) }, "Close")) + ]), + form({ method: 'dialog', class: 'modal-backdrop', onclick: () => p.$open(false) }, button("close")) + ]) : null; - /** _input @param {Object} p */ - ui._input = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ - p.label && div({ class: 'flex items-center gap-2' }, [ - span(p.label), - p.tip && div({ class: 'tooltip tooltip-right', 'data-tip': p.tip }, span({ class: 'badge badge-ghost badge-xs' }, '?')) - ]), - $.html('input', { ...p, class: parseClass('input input-bordered w-full', p.$class || p.class), $value: p.$value }), - () => p.$error?.() ? span({ class: 'text-error text-xs' }, p.$error()) : null - ]); + /** + * Dropdown menu component. + */ + ui._dropdown = (p, c) => div({ ...p, class: parseClass('dropdown', p.$class || p.class) }, [ + div({ tabindex: 0, role: 'button', class: 'btn m-1' }, p.label), + div({ tabindex: 0, class: 'dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52' }, c) + ]); - /** _select @param {Object} p */ - ui._select = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [ - p.label && span(p.label), - select({ - ...p, - class: parseClass('select select-bordered', p.$class || p.class), - onchange: (e) => p.$value?.(e.target.value) - }, (p.options || []).map(o => $.html('option', { value: o.value, selected: o.value === p.$value?.() }, o.label))) - ]); + /** + * Accordion/Collapse component. + */ + ui._accordion = (p, c) => div({ class: 'collapse collapse-arrow bg-base-200 mb-2' }, [ + $.html('input', { type: p.name ? 'radio' : 'checkbox', name: p.name, checked: p.open }), + div({ class: 'collapse-title text-xl font-medium' }, p.title), + div({ class: 'collapse-content' }, c) + ]); - /** _checkbox @param {Object} p */ - ui._checkbox = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ - $.html('input', { type: 'checkbox', ...p, class: parseClass('checkbox', p.$class || p.class), $checked: p.$value }), - p.label && span({ class: 'label-text' }, p.label) - ]); + /** + * Tabs navigation component. + * @param {Object} p - Tab properties. + * @param {Array<{label: string, active: boolean|function, onclick: function}>} p.items - Tab items. + */ + ui._tabs = (p) => div({ role: 'tablist', class: parseClass('tabs tabs-lifted', p.$class || p.class) }, + (p.items || []).map(it => a({ + role: 'tab', + class: () => `tab ${ (typeof it.active === 'function' ? it.active() : it.active) ? 'tab-active' : '' }`, + onclick: it.onclick + }, it.label)) + ); - /** _radio @param {Object} p */ - ui._radio = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [ - $.html('input', { - type: 'radio', ...p, - class: parseClass('radio', p.$class || p.class), - $checked: () => p.$value?.() === p.value, - onclick: () => p.$value?.(p.value) - }), - p.label && span({ class: 'label-text' }, p.label) - ]); + /** + * Badge component. + */ + ui._badge = (p, c) => span({ ...p, class: parseClass('badge', p.$class || p.class) }, c); - /** _range @param {Object} p */ - ui._range = (p) => div({ class: 'flex flex-col gap-2' }, [ - p.label && span({ class: 'label-text' }, p.label), - $.html('input', { type: 'range', ...p, class: parseClass('range', p.$class || p.class), $value: p.$value }) - ]); + /** + * Tooltip wrapper. + */ + ui._tooltip = (p, c) => div({ ...p, class: parseClass('tooltip', p.$class || p.class), 'data-tip': p.tip }, c); - /** _modal @param {Object} p @param {any} c */ - ui._modal = (p, c) => () => p.$open() ? dialog({ class: 'modal modal-open' }, [ - div({ class: 'modal-box' }, [ - p.title && h3({ class: 'text-lg font-bold mb-4' }, p.title), - c, - div({ class: 'modal-action' }, ui._button({ onclick: () => p.$open(false) }, "Cerrar")) - ]), - form({ method: 'dialog', class: 'modal-backdrop', onclick: () => p.$open(false) }, button("close")) - ]) : null; + /** + * Navbar component. + */ + ui._navbar = (p, c) => div({ ...p, class: parseClass('navbar bg-base-100 shadow-sm px-4', p.$class || p.class) }, c); - /** _dropdown @param {Object} p @param {any} c */ - ui._dropdown = (p, c) => div({ ...p, class: parseClass('dropdown', p.$class || p.class) }, [ - div({ tabindex: 0, role: 'button', class: 'btn m-1' }, p.label), - div({ tabindex: 0, class: 'dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52' }, c) - ]); + /** + * Vertical Menu component. + */ + ui._menu = (p) => ul({ ...p, class: parseClass('menu bg-base-200 rounded-box', p.$class || p.class) }, + (p.items || []).map(it => li({}, a({ + class: () => (typeof it.active === 'function' ? it.active() : it.active) ? 'active' : '', + onclick: it.onclick + }, [it.icon && span({class:'mr-2'}, it.icon), it.label]))) + ); - /** _accordion @param {Object} p @param {any} c */ - ui._accordion = (p, c) => div({ class: 'collapse collapse-arrow bg-base-200 mb-2' }, [ - $.html('input', { type: p.name ? 'radio' : 'checkbox', name: p.name, checked: p.open }), - div({ class: 'collapse-title text-xl font-medium' }, p.title), - div({ class: 'collapse-content' }, c) - ]); + /** + * Sidebar Drawer component. + */ + ui._drawer = (p) => div({ class: 'drawer' }, [ + $.html('input', { id: p.id, type: 'checkbox', class: 'drawer-toggle', $checked: p.$open }), + div({ class: 'drawer-content' }, p.content), + div({ class: 'drawer-side' }, [ + label({ for: p.id, class: 'drawer-overlay', onclick: () => p.$open?.(false) }), + div({ class: 'min-h-full bg-base-200 w-80' }, p.side) + ]) + ]); - /** _tabs @param {Object} p */ - ui._tabs = (p) => div({ role: 'tablist', class: parseClass('tabs tabs-lifted', p.$class || p.class) }, - (p.items || []).map(it => a({ - role: 'tab', - class: () => `tab ${ (typeof it.active === 'function' ? it.active() : it.active) ? 'tab-active' : '' }`, - onclick: it.onclick - }, it.label)) - ); + /** + * Fieldset wrapper with legend. + */ + ui._fieldset = (p, c) => fieldset({ + ...p, + class: parseClass('fieldset bg-base-200 border border-base-300 p-4 rounded-lg', p.$class || p.class) + }, [ + p.legend && legend({ class: 'fieldset-legend font-bold' }, p.legend), + c + ]); - /** _badge @param {Object} p @param {any} c */ - ui._badge = (p, c) => span({ ...p, class: parseClass('badge', p.$class || p.class) }, c); - - /** _tooltip @param {Object} p @param {any} c */ - ui._tooltip = (p, c) => div({ ...p, class: parseClass('tooltip', p.$class || p.class), 'data-tip': p.tip }, c); - - /** _navbar @param {Object} p @param {any} c */ - ui._navbar = (p, c) => div({ ...p, class: parseClass('navbar bg-base-100 shadow-sm px-4', p.$class || p.class) }, c); - - /** _menu @param {Object} p */ - ui._menu = (p) => ul({ ...p, class: parseClass('menu bg-base-200 rounded-box', p.$class || p.class) }, - (p.items || []).map(it => li({}, a({ - class: () => (typeof it.active === 'function' ? it.active() : it.active) ? 'active' : '', - onclick: it.onclick - }, [it.icon && span({class:'mr-2'}, it.icon), it.label]))) - ); - - /** _drawer @param {Object} p */ - ui._drawer = (p) => div({ class: 'drawer' }, [ - $.html('input', { id: p.id, type: 'checkbox', class: 'drawer-toggle', $checked: p.$open }), - div({ class: 'drawer-content' }, p.content), - div({ class: 'drawer-side' }, [ - label({ for: p.id, class: 'drawer-overlay', onclick: () => p.$open?.(false) }), - div({ class: 'min-h-full bg-base-200 w-80' }, p.side) - ]) - ]); - - /** _fieldset @param {Object} p @param {any} c */ - ui._fieldset = (p, c) => fieldset({ ...p, class: parseClass('fieldset bg-base-200 border border-base-300 p-4 rounded-lg', p.$class || p.class) }, [ - p.legend && legend({ class: 'fieldset-legend font-bold' }, p.legend), - c - ]); - - // Expose components to window and $ object - Object.keys(ui).forEach(key => { - window[key] = ui[key]; - $[key] = ui[key]; - }); + // Expose components globally and to the SigPro instance + Object.keys(ui).forEach(key => { + window[key] = ui[key]; + $[key] = ui[key]; }); -})(); \ No newline at end of file +}); \ No newline at end of file diff --git a/src/sigpro/sigpro.js b/src/sigpro/sigpro.js index 94b9702..8be1a9a 100644 --- a/src/sigpro/sigpro.js +++ b/src/sigpro/sigpro.js @@ -1,31 +1,55 @@ /** * SigPro 2.0 - Atomic Unified Reactive Engine + * A lightweight, fine-grained reactivity system with built-in routing and plugin support. + * @author Gemini & User */ (() => { - /** @type {Function|null} */ + /** @type {Function|null} Internal tracker for the currently executing reactive effect. */ 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. + * @property {function(string, Object=, any=): HTMLElement} html - Creates a reactive HTML element. + * @property {function((HTMLElement|function), (HTMLElement|string)=): void} mount - Mounts a component to the DOM. + * @property {function(Array): HTMLElement} router - Initializes a hash-based router. + * @property {function(string): void} router.go - Programmatic navigation to a hash path. + * @property {function((function|string|Array)): (Promise|SigPro)} plugin - Extends SigPro or loads external scripts. */ /** - * 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. + * Creates a Signal (state) or a Computed/Effect (reaction). + * @param {any|function} initial - Initial value for a signal, or a function for computed logic. + * @returns {Function} A reactive accessor/mutator function. + * @example + * const $count = $(0); // Signal: $count(5) to update, $count() to read. + * const $double = $(() => $count() * 2); // Computed: Auto-updates when $count changes. */ const $ = (initial) => { const subs = new Set(); - const signal = (...args) => { + + // Logic for Computed Signals (Functions) + if (typeof initial === 'function') { + let cached; + const runner = () => { + const prev = activeEffect; + activeEffect = runner; + try { + const next = initial(); + if (!Object.is(cached, next)) { + cached = next; + subs.forEach(s => s()); + } + } finally { activeEffect = prev; } + }; + runner(); + return () => { + if (activeEffect) subs.add(activeEffect); + return cached; + }; + } + + // Logic for Standard Signals (State) + return (...args) => { if (args.length) { const next = typeof args[0] === 'function' ? args[0](initial) : args[0]; if (!Object.is(initial, next)) { @@ -36,28 +60,14 @@ 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. + * Hyperscript engine to render reactive HTML nodes. + * @param {string} tag - The HTML tag name (e.g., 'div', 'button'). + * @param {Object} [props] - Attributes, events (onclick), or reactive props ($value, $class). * @param {any} [content] - String, Node, Array of nodes, or reactive function. - * @returns {HTMLElement} A live DOM element. + * @returns {HTMLElement} A live DOM element linked to SigPro signals. */ $.html = (tag, props = {}, content = []) => { const el = document.createElement(tag); @@ -71,10 +81,12 @@ el.addEventListener(key.toLowerCase().slice(2), val); } else if (key.startsWith('$')) { const attr = key.slice(1); + // Two-way binding for inputs 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)); } + // Reactive attribute update $(() => { const v = typeof val === 'function' ? val() : val; if (attr === 'value' || attr === 'checked') el[attr] = v; @@ -106,8 +118,8 @@ /** * 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. + * @param {HTMLElement|function} node - Root component or element to mount. + * @param {HTMLElement|string} [target=document.body] - Target element or CSS selector. */ $.mount = (node, target = document.body) => { const el = typeof target === 'string' ? document.querySelector(target) : target; @@ -119,9 +131,8 @@ /** * 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. + * @param {Array<{path: string, component: function|HTMLElement}>} routes - Route definitions. + * @returns {HTMLElement} A container that swaps content based on window.location.hash. */ $.router = (routes) => { const sPath = $(window.location.hash.replace(/^#/, "") || "/"); @@ -137,33 +148,49 @@ 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"); + const component = route + ? (typeof route.component === 'function' ? route.component() : route.component) + : $.html('h1', "404 - Not Found"); + return component instanceof Node ? component : $.html('span', String(component)); } ]); }; /** - * Programmatic navigation. - * @param {string} path - The path to navigate to (e.g., '/home'). + * Programmatic navigation for the SigPro Router. + * @param {string} path - The path to navigate to (e.g., '/dashboard'). */ $.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. + * Polymorphic Plugin System. + * Registers internal functions or loads external .js files as plugins. + * @param {function|string|Array} source - Plugin function or URL(s). + * @returns {Promise|SigPro} Resolves with the $ instance after loading or registering. */ - $.use = (plugin, options = {}) => { - if (typeof plugin === 'function') plugin($, options); + $.plugin = (source) => { + if (typeof source === 'function') { + source($); + return $; + } + const urls = Array.isArray(source) ? source : [source]; + return Promise.all(urls.map(url => new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.onload = () => { + console.log(`%c[SigPro] Plugin Loaded: ${url}`, "color: #51cf66; font-weight: bold;"); + resolve(); + }; + script.onerror = () => reject(new Error(`[SigPro] Failed to load: ${url}`)); + document.head.appendChild(script); + }))).then(() => $); }; + // Global HTML Tag Proxy Helpers 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));