New docs
This commit is contained in:
41
plugins/debug.js
Normal file
41
plugins/debug.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* SigPro Debug Plugin
|
||||
* Reactive state logger for signals and computed values.
|
||||
*/
|
||||
export const Debug = ($) => {
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
};
|
||||
};
|
||||
39
plugins/fetch.js
Normal file
39
plugins/fetch.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* SigPro Fetch Plugin
|
||||
* Adds reactive data fetching capabilities to the SigPro instance.
|
||||
* @param {SigPro} $ - The SigPro core instance.
|
||||
*/
|
||||
export const Fetch = ($) => {
|
||||
/**
|
||||
* 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);
|
||||
const $loading = $(true);
|
||||
const $error = $(null);
|
||||
|
||||
fetch(url, options)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP error! Status: ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(json => $data(json))
|
||||
.catch(err => {
|
||||
console.error("[SigPro Fetch Error]:", err);
|
||||
$error(err.message);
|
||||
})
|
||||
.finally(() => $loading(false));
|
||||
|
||||
return { $data, $loading, $error };
|
||||
};
|
||||
};
|
||||
6
plugins/index.js
Normal file
6
plugins/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// /plugins/index.js
|
||||
export { UI } from './ui.js';
|
||||
export { Fetch } from './fetch.js';
|
||||
export { Storage } from './storage.js';
|
||||
export { Debug } from './debug.js';
|
||||
export { Router } from './router.js';
|
||||
44
plugins/router.js
Normal file
44
plugins/router.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// plugins/router.js
|
||||
export const Router = ($) => {
|
||||
|
||||
$.router = (routes) => {
|
||||
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
|
||||
|
||||
return 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 === "*");
|
||||
|
||||
if (!route) return h1("404 - Not Found");
|
||||
|
||||
// --- LA MEJORA AQUÍ ---
|
||||
const result = typeof route.component === 'function' ? route.component() : route.component;
|
||||
|
||||
// Si el componente es una Promesa (Lazy Loading de Vite), esperamos
|
||||
if (result instanceof Promise) {
|
||||
const $lazyNode = $(span("Cargando página..."));
|
||||
result.then(m => {
|
||||
// Si el módulo tiene un .default (export default), lo usamos
|
||||
const comp = typeof m === 'function' ? m() : (m.default ? m.default() : m);
|
||||
$lazyNode(comp);
|
||||
});
|
||||
return () => $lazyNode();
|
||||
}
|
||||
|
||||
return result instanceof Node ? result : span(String(result));
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
$.router.go = (path) => {
|
||||
window.location.hash = path.startsWith('/') ? path : `/${path}`;
|
||||
};
|
||||
|
||||
window._router = $.router;
|
||||
};
|
||||
31
plugins/storage.js
Normal file
31
plugins/storage.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* SigPro Storage Plugin
|
||||
* Automatically synchronizes signals with localStorage.
|
||||
*/
|
||||
export const Storage = ($) => {
|
||||
/**
|
||||
* Persists a signal's value in localStorage.
|
||||
* @param {Function} $sig - The signal to persist.
|
||||
* @param {string} key - The localStorage key name.
|
||||
* @returns {Function} The same signal for chaining.
|
||||
*/
|
||||
_storage = ($sig, key) => {
|
||||
// 1. Initial Load: If there's data in storage, update the signal immediately
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved !== null) {
|
||||
try {
|
||||
$sig(JSON.parse(saved));
|
||||
} catch (e) {
|
||||
console.error(`[SigPro Storage] Error parsing key "${key}":`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Auto-Save: Every time the signal changes, update localStorage
|
||||
$(() => {
|
||||
const val = $sig();
|
||||
localStorage.setItem(key, JSON.stringify(val));
|
||||
});
|
||||
|
||||
return $sig;
|
||||
};
|
||||
};
|
||||
207
plugins/ui.js
Normal file
207
plugins/ui.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* SigPro UI 2.0 - daisyUI v5 & Tailwind v4 Plugin
|
||||
* Provides a set of reactive functional components.
|
||||
*/
|
||||
|
||||
export const UI = ($) => {
|
||||
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 || ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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)
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
div({ class: 'modal-action' }, ui._button({ onclick: () => p.$open(false) }, "Close"))
|
||||
]),
|
||||
form({ method: 'dialog', class: 'modal-backdrop', onclick: () => p.$open(false) }, button("close"))
|
||||
]) : 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)
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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))
|
||||
);
|
||||
|
||||
/**
|
||||
* Badge component.
|
||||
*/
|
||||
ui._badge = (p, c) => span({ ...p, class: parseClass('badge', p.$class || p.class) }, c);
|
||||
|
||||
/**
|
||||
* Tooltip wrapper.
|
||||
*/
|
||||
ui._tooltip = (p, c) => div({ ...p, class: parseClass('tooltip', p.$class || p.class), 'data-tip': p.tip }, c);
|
||||
|
||||
/**
|
||||
* Navbar component.
|
||||
*/
|
||||
ui._navbar = (p, c) => div({ ...p, class: parseClass('navbar bg-base-100 shadow-sm px-4', p.$class || p.class) }, 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])))
|
||||
);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
])
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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
|
||||
]);
|
||||
|
||||
// Expose components globally and to the SigPro instance
|
||||
Object.keys(ui).forEach(key => {
|
||||
window[key] = ui[key];
|
||||
$[key] = ui[key];
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user