update
This commit is contained in:
41
src/plugins/debug.js
Normal file
41
src/plugins/debug.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* SigPro Fetch Plugin
|
* 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.
|
* Performs a reactive asynchronous fetch request.
|
||||||
* @param {string} url - The API endpoint.
|
* @param {string} url - The URL of the resource to fetch.
|
||||||
* @param {Object} [options] - Fetch options (method, headers, etc).
|
* @param {RequestInit} [options] - Optional settings for the fetch request (method, headers, body, etc.).
|
||||||
* @returns {{ $data: Function, $loading: Function, $error: Function }}
|
* @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 = {}) => {
|
$.fetch = (url, options = {}) => {
|
||||||
const $data = $(null);
|
const $data = $(null);
|
||||||
@@ -16,11 +23,14 @@ $.use(($) => {
|
|||||||
|
|
||||||
fetch(url, options)
|
fetch(url, options)
|
||||||
.then(res => {
|
.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();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(json => $data(json))
|
.then(json => $data(json))
|
||||||
.catch(err => $error(err.message))
|
.catch(err => {
|
||||||
|
console.error("[SigPro Fetch Error]:", err);
|
||||||
|
$error(err.message);
|
||||||
|
})
|
||||||
.finally(() => $loading(false));
|
.finally(() => $loading(false));
|
||||||
|
|
||||||
return { $data, $loading, $error };
|
return { $data, $loading, $error };
|
||||||
|
|||||||
@@ -1,141 +1,206 @@
|
|||||||
/**
|
/**
|
||||||
* SigPro UI 2.0 - daisyUI v5 & Tailwind v4 Plugin
|
* SigPro UI 2.0 - daisyUI v5 & Tailwind v4 Plugin
|
||||||
|
* Provides a set of reactive functional components.
|
||||||
*/
|
*/
|
||||||
(() => {
|
$.plugin(($) => {
|
||||||
if (!window.$) return console.error("[SigPro UI] Fatal: SigPro 2.0 Core not found.");
|
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() || ''}`;
|
* Standard Button component.
|
||||||
return `${base} ${extra || ''}`;
|
* @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({
|
* Form Input with label, tooltip, and error handling.
|
||||||
...p,
|
* @param {Object} p - Input properties.
|
||||||
class: parseClass('btn', p.$class || p.class),
|
* @param {string} [p.label] - Field label.
|
||||||
$disabled: () => p.$disabled?.() || p.$loading?.()
|
* @param {string} [p.tip] - Tooltip text.
|
||||||
}, [
|
* @param {function} [p.$value] - Reactive signal for the value.
|
||||||
() => p.$loading?.() ? span({ class: 'loading loading-spinner' }) : null,
|
* @param {function} [p.$error] - Reactive signal for error messages.
|
||||||
p.icon && span({ class: 'mr-1' }, p.icon),
|
*/
|
||||||
|
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,
|
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' }, [
|
* Dropdown menu component.
|
||||||
p.label && div({ class: 'flex items-center gap-2' }, [
|
*/
|
||||||
span(p.label),
|
ui._dropdown = (p, c) => div({ ...p, class: parseClass('dropdown', p.$class || p.class) }, [
|
||||||
p.tip && div({ class: 'tooltip tooltip-right', 'data-tip': p.tip }, span({ class: 'badge badge-ghost badge-xs' }, '?'))
|
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)
|
||||||
$.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 @param {Object} p */
|
/**
|
||||||
ui._select = (p) => label({ class: 'fieldset-label flex flex-col gap-1' }, [
|
* Accordion/Collapse component.
|
||||||
p.label && span(p.label),
|
*/
|
||||||
select({
|
ui._accordion = (p, c) => div({ class: 'collapse collapse-arrow bg-base-200 mb-2' }, [
|
||||||
...p,
|
$.html('input', { type: p.name ? 'radio' : 'checkbox', name: p.name, checked: p.open }),
|
||||||
class: parseClass('select select-bordered', p.$class || p.class),
|
div({ class: 'collapse-title text-xl font-medium' }, p.title),
|
||||||
onchange: (e) => p.$value?.(e.target.value)
|
div({ class: 'collapse-content' }, c)
|
||||||
}, (p.options || []).map(o => $.html('option', { value: o.value, selected: o.value === p.$value?.() }, o.label)))
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
/** _checkbox @param {Object} p */
|
/**
|
||||||
ui._checkbox = (p) => label({ class: 'label cursor-pointer justify-start gap-3' }, [
|
* Tabs navigation component.
|
||||||
$.html('input', { type: 'checkbox', ...p, class: parseClass('checkbox', p.$class || p.class), $checked: p.$value }),
|
* @param {Object} p - Tab properties.
|
||||||
p.label && span({ class: 'label-text' }, p.label)
|
* @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' }, [
|
* Badge component.
|
||||||
$.html('input', {
|
*/
|
||||||
type: 'radio', ...p,
|
ui._badge = (p, c) => span({ ...p, class: parseClass('badge', p.$class || p.class) }, c);
|
||||||
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 @param {Object} p */
|
/**
|
||||||
ui._range = (p) => div({ class: 'flex flex-col gap-2' }, [
|
* Tooltip wrapper.
|
||||||
p.label && span({ class: 'label-text' }, p.label),
|
*/
|
||||||
$.html('input', { type: 'range', ...p, class: parseClass('range', p.$class || p.class), $value: p.$value })
|
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' }, [
|
* Navbar component.
|
||||||
div({ class: 'modal-box' }, [
|
*/
|
||||||
p.title && h3({ class: 'text-lg font-bold mb-4' }, p.title),
|
ui._navbar = (p, c) => div({ ...p, class: parseClass('navbar bg-base-100 shadow-sm px-4', p.$class || p.class) }, c);
|
||||||
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;
|
|
||||||
|
|
||||||
/** _dropdown @param {Object} p @param {any} c */
|
/**
|
||||||
ui._dropdown = (p, c) => div({ ...p, class: parseClass('dropdown', p.$class || p.class) }, [
|
* Vertical Menu component.
|
||||||
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)
|
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' }, [
|
* Sidebar Drawer component.
|
||||||
$.html('input', { type: p.name ? 'radio' : 'checkbox', name: p.name, checked: p.open }),
|
*/
|
||||||
div({ class: 'collapse-title text-xl font-medium' }, p.title),
|
ui._drawer = (p) => div({ class: 'drawer' }, [
|
||||||
div({ class: 'collapse-content' }, c)
|
$.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) },
|
* Fieldset wrapper with legend.
|
||||||
(p.items || []).map(it => a({
|
*/
|
||||||
role: 'tab',
|
ui._fieldset = (p, c) => fieldset({
|
||||||
class: () => `tab ${ (typeof it.active === 'function' ? it.active() : it.active) ? 'tab-active' : '' }`,
|
...p,
|
||||||
onclick: it.onclick
|
class: parseClass('fieldset bg-base-200 border border-base-300 p-4 rounded-lg', p.$class || p.class)
|
||||||
}, it.label))
|
}, [
|
||||||
);
|
p.legend && legend({ class: 'fieldset-legend font-bold' }, p.legend),
|
||||||
|
c
|
||||||
|
]);
|
||||||
|
|
||||||
/** _badge @param {Object} p @param {any} c */
|
// Expose components globally and to the SigPro instance
|
||||||
ui._badge = (p, c) => span({ ...p, class: parseClass('badge', p.$class || p.class) }, c);
|
Object.keys(ui).forEach(key => {
|
||||||
|
window[key] = ui[key];
|
||||||
/** _tooltip @param {Object} p @param {any} c */
|
$[key] = ui[key];
|
||||||
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];
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})();
|
});
|
||||||
@@ -1,31 +1,55 @@
|
|||||||
/**
|
/**
|
||||||
* SigPro 2.0 - Atomic Unified Reactive Engine
|
* 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;
|
let activeEffect = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} SigPro
|
* @typedef {Object} SigPro
|
||||||
* @property {function(string, Object=, any=): HTMLElement} html - Creates a reactive HTML element. Supports attributes, events, and reactive signals.
|
* @property {function(string, Object=, any=): HTMLElement} html - Creates a reactive HTML element.
|
||||||
* @property {function((HTMLElement|function), (HTMLElement|string)=): void} mount - Clears the target and appends the root component to the DOM.
|
* @property {function((HTMLElement|function), (HTMLElement|string)=): void} mount - Mounts a component to the DOM.
|
||||||
* @property {function(Array): HTMLElement} router - Initializes a hash-based router that swaps components reactively.
|
* @property {function(Array<Object>): HTMLElement} router - Initializes a hash-based router.
|
||||||
* @property {function(function, Object=): void} use - Extends SigPro functionality by injecting the $ object into a plugin function.
|
* @property {function(string): void} router.go - Programmatic navigation to a hash path.
|
||||||
|
* @property {function((function|string|Array<string>)): (Promise<SigPro>|SigPro)} plugin - Extends SigPro or loads external scripts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Signal, Computed, or Effect.
|
* Creates a Signal (state) or a Computed/Effect (reaction).
|
||||||
* - If a value is passed: Creates a Signal.
|
* @param {any|function} initial - Initial value for a signal, or a function for computed logic.
|
||||||
* - If a function is passed: Creates a Computed/Effect that auto-tracks dependencies.
|
* @returns {Function} A reactive accessor/mutator function.
|
||||||
* * @example
|
* @example
|
||||||
* const $count = $(0); // Signal
|
* const $count = $(0); // Signal: $count(5) to update, $count() to read.
|
||||||
* const $double = $(() => $count() * 2); // Computed
|
* const $double = $(() => $count() * 2); // Computed: Auto-updates when $count changes.
|
||||||
* * @param {any} initial - The initial value or a reactive function.
|
|
||||||
* @returns {Function & SigPro} Reactive accessor/mutator with SigPro tools attached.
|
|
||||||
*/
|
*/
|
||||||
const $ = (initial) => {
|
const $ = (initial) => {
|
||||||
const subs = new Set();
|
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) {
|
if (args.length) {
|
||||||
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
|
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
|
||||||
if (!Object.is(initial, next)) {
|
if (!Object.is(initial, next)) {
|
||||||
@@ -36,28 +60,14 @@
|
|||||||
if (activeEffect) subs.add(activeEffect);
|
if (activeEffect) subs.add(activeEffect);
|
||||||
return initial;
|
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.
|
* Hyperscript engine to render reactive 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 {string} tag - The HTML tag name (e.g., 'div', 'button').
|
* @param {Object} [props] - Attributes, events (onclick), or reactive props ($value, $class).
|
||||||
* @param {Object} [props] - Object containing attributes, events, or $reactive_props.
|
|
||||||
* @param {any} [content] - String, Node, Array of nodes, or reactive function.
|
* @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 = []) => {
|
$.html = (tag, props = {}, content = []) => {
|
||||||
const el = document.createElement(tag);
|
const el = document.createElement(tag);
|
||||||
@@ -71,10 +81,12 @@
|
|||||||
el.addEventListener(key.toLowerCase().slice(2), val);
|
el.addEventListener(key.toLowerCase().slice(2), val);
|
||||||
} else if (key.startsWith('$')) {
|
} else if (key.startsWith('$')) {
|
||||||
const attr = key.slice(1);
|
const attr = key.slice(1);
|
||||||
|
// Two-way binding for inputs
|
||||||
if ((attr === 'value' || attr === 'checked') && typeof val === 'function') {
|
if ((attr === 'value' || attr === 'checked') && typeof val === 'function') {
|
||||||
const ev = attr === 'checked' ? 'change' : 'input';
|
const ev = attr === 'checked' ? 'change' : 'input';
|
||||||
el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value));
|
el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value));
|
||||||
}
|
}
|
||||||
|
// Reactive attribute update
|
||||||
$(() => {
|
$(() => {
|
||||||
const v = typeof val === 'function' ? val() : val;
|
const v = typeof val === 'function' ? val() : val;
|
||||||
if (attr === 'value' || attr === 'checked') el[attr] = v;
|
if (attr === 'value' || attr === 'checked') el[attr] = v;
|
||||||
@@ -106,8 +118,8 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Application mounter.
|
* Application mounter.
|
||||||
* @param {HTMLElement|Function} node - The root component or element to mount.
|
* @param {HTMLElement|function} node - Root component or element to mount.
|
||||||
* @param {HTMLElement|string} [target] - The DOM element or selector where the app will be injected.
|
* @param {HTMLElement|string} [target=document.body] - Target element or CSS selector.
|
||||||
*/
|
*/
|
||||||
$.mount = (node, target = document.body) => {
|
$.mount = (node, target = document.body) => {
|
||||||
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
||||||
@@ -119,9 +131,8 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reactive Client-side Hash Router.
|
* Reactive Client-side Hash Router.
|
||||||
* Tracks window.location.hash and renders the matching component.
|
* @param {Array<{path: string, component: function|HTMLElement}>} routes - Route definitions.
|
||||||
* * @param {Array<{path: string, component: Function|HTMLElement}>} routes - Array of route definitions.
|
* @returns {HTMLElement} A container that swaps content based on window.location.hash.
|
||||||
* @returns {HTMLElement} A reactive container that updates on route change.
|
|
||||||
*/
|
*/
|
||||||
$.router = (routes) => {
|
$.router = (routes) => {
|
||||||
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||||
@@ -137,33 +148,49 @@
|
|||||||
return rP.every((part, i) => part.startsWith(':') || part === cP[i]);
|
return rP.every((part, i) => part.startsWith(':') || part === cP[i]);
|
||||||
}) || routes.find(r => r.path === "*");
|
}) || routes.find(r => r.path === "*");
|
||||||
|
|
||||||
let component = route
|
const component = route
|
||||||
? (typeof route.component === 'function' ? route.component() : route.component)
|
? (typeof route.component === 'function' ? route.component() : route.component)
|
||||||
: $.html('h1', "404");
|
: $.html('h1', "404 - Not Found");
|
||||||
|
|
||||||
if (!component) return $.html('h1', "404");
|
|
||||||
return component instanceof Node ? component : $.html('span', String(component));
|
return component instanceof Node ? component : $.html('span', String(component));
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Programmatic navigation.
|
* Programmatic navigation for the SigPro Router.
|
||||||
* @param {string} path - The path to navigate to (e.g., '/home').
|
* @param {string} path - The path to navigate to (e.g., '/dashboard').
|
||||||
*/
|
*/
|
||||||
$.router.go = (path) => {
|
$.router.go = (path) => {
|
||||||
window.location.hash = path.startsWith('/') ? path : `/${path}`;
|
window.location.hash = path.startsWith('/') ? path : `/${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SigPro Plugin System.
|
* Polymorphic Plugin System.
|
||||||
* @param {function(SigPro, Object): void} plugin - Function that receives the SigPro instance.
|
* Registers internal functions or loads external .js files as plugins.
|
||||||
* @param {Object} [options] - Configuration for the plugin.
|
* @param {function|string|Array<string>} source - Plugin function or URL(s).
|
||||||
|
* @returns {Promise<SigPro>|SigPro} Resolves with the $ instance after loading or registering.
|
||||||
*/
|
*/
|
||||||
$.use = (plugin, options = {}) => {
|
$.plugin = (source) => {
|
||||||
if (typeof plugin === 'function') plugin($, options);
|
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'];
|
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));
|
tags.forEach(t => window[t] = (p, c) => $.html(t, p, c));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user