UUUUPPPPP work

This commit is contained in:
2026-03-25 02:03:59 +01:00
parent c857900860
commit df8bd891a2
32 changed files with 1126 additions and 233 deletions

View File

@@ -13,7 +13,6 @@
"mariadb": "^3.5.2",
"nodemailer": "^8.0.2",
"sharp": "^0.34.5",
"sigpro": "^1.0.9",
},
"devDependencies": {
"@iconify/json": "^2.2.443",
@@ -390,8 +389,6 @@
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"sigpro": ["sigpro@1.0.9", "", {}, "sha512-Vq+7OH0qCsku8P0hK511NXeWY8yrsn1cgTgs7tuv9jHp3PiSBQ1b3oWn3XSSDzUpiLVawF3tTRRbo2/63CKQog=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],

View File

@@ -21,8 +21,7 @@
"hono": "^4.12.7",
"mariadb": "^3.5.2",
"nodemailer": "^8.0.2",
"sharp": "^0.34.5",
"sigpro": "^1.0.9"
"sharp": "^0.34.5"
},
"devDependencies": {
"@iconify/json": "^2.2.443",

63
router.js Normal file
View File

@@ -0,0 +1,63 @@
import fs from 'fs';
import path from 'path';
export function sigproRouter() {
const virtualModuleId = 'virtual:sigpro-routes';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
const getFiles = (dir) => {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir, { recursive: true })
.filter(file => /\.(js|jsx)$/.test(file) && !path.basename(file).startsWith('_'))
.map(file => path.resolve(dir, file));
};
const pathToUrl = (pagesDir, filePath) => {
let relative = path.relative(pagesDir, filePath)
.replace(/\\/g, '/')
.replace(/\.(js|jsx)$/, '')
.replace(/\/index$/, '')
.replace(/^index$/, '');
let url = '/' + relative;
return url
.replace(/\/+/g, '/')
.replace(/\[\.\.\.([^\]]+)\]/g, '*')
.replace(/\[([^\]]+)\]/g, ':$1')
.replace(/\/$/, '') || '/';
};
return {
name: 'sigpro-router',
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
},
load(id) {
if (id !== resolvedVirtualModuleId) return;
const pagesDir = path.resolve(process.cwd(), 'src/pages');
const files = getFiles(pagesDir).sort((a, b) => {
const urlA = pathToUrl(pagesDir, a);
const urlB = pathToUrl(pagesDir, b);
if (urlA.includes(':') && !urlB.includes(':')) return 1;
if (!urlA.includes(':') && urlB.includes(':')) return -1;
return urlB.length - urlA.length;
});
let routeEntries = '';
files.forEach((fullPath) => {
const urlPath = pathToUrl(pagesDir, fullPath);
const importPath = fullPath.replace(/\\/g, '/');
routeEntries += ` { path: '${urlPath}', component: () => import('${importPath}') },\n`;
});
if (!routeEntries.includes("path: '*'")) {
routeEntries += ` { path: '*', component: () => span('404 - Not Found') },\n`;
}
return `export const routes = [\n${routeEntries}];`;
}
};
}

View File

@@ -1,136 +1,131 @@
import { html, $ } from "sigpro";
import { isDark } from "./store.js";
import { routes } from 'virtual:sigpro-routes';
import "@components/Drawer.js";
import "@components/Menu.js";
import "@components/Button.js";
// App.js
import { $ } from "./sigpro";
/**
* Vistas de la aplicación (pueden ir en archivos separados luego)
*/
const $valor = $("");
const $toggle = $(false);
// const consoleToggle = $(()=>console.log($toggle()))
const Home = () => {
// --- COMPONENTE: BUSCADOR ---
const SearchBar = (buscar) => html`
<label class="floating-label">
<span>Buscar</span>
<label class="input">
<input type="text" class="input" :value="${buscar}" @input=${(e) => buscar(e.target.value)} placeholder="Buscar..." />
<span ?hidden=${() => buscar().length === 0} class="btn btn-sm btn-ghost icon-[lucide--x]" @click=${() => buscar("")}></span>
<span class="icon-[lucide--search]"></span>
</label>
</label>
`;
// --- COMPONENTE: MENU USUARIO ---
const UserMenu = () => html`
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-8 rounded-full flex items-center justify-center">
<span class="icon-[lucide--user] size-5"></span>
</div>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-100 w-52 p-2 shadow-2xl mt-3 border border-base-300">
<li class="menu-title"><span>Cuenta</span></li>
<li>
<a href="#/profile">
<span class="icon-[lucide--user-cog] size-4"></span>
Perfil
</a>
</li>
<li>
<a href="#/settings">
<span class="icon-[lucide--settings] size-4"></span>
Ajustes
</a>
</li>
<div class="divider my-0"></div>
<li>
<a class="text-error">
<span class="icon-[lucide--log-out] size-4"></span>
Salir
</a>
</li>
</ul>
</div>
`;
export default function App() {
const openMenu = $(false);
const buscar = $("");
// Array de configuración (Data)
const menuConfig = [
{ label: "Panel Principal", icon: "icon-[lucide--home]", href: "#/", active: true },
{ label: "Perfil", href: "#/profile", icon: "icon-[lucide--user]" },
const miCheck = $(false); // Creamos la señal
return Div({ class: "prose" }, [
H1("Dashboard Principal"),
Indicator({ badge: "22" }, [Button({ class: "btn" }, "Hola")]),
Button({ tooltip: "tooltip", badge: "22" }, "Hola"),
Input({
label: "Correo Electrónico",
type: "email",
placeholder: "ejemplo@correo.com",
$value: $valor, // Binding automático
}),
Span({ class: "gap-4 text-4xl" }, () => $valor()),
P("Bienvenido a la interfaz reactiva de SigPro. Aquí puedes ver el estado global."),
CheckBox({ tooltip: "Tooltip", toggle: $toggle, $value: miCheck, label: "Checkbox" }),
miCheck,
$toggle,
Button(
{
label: "Clientes",
icon: "icon-[lucide--database]",
open: false,
sub: [
{ label: "Clientes Activos", icon: "icon-[lucide--users]", href: "#/users", onClick: (i) => console.log("Users", i) },
{ label: "Todos", icon: "icon-[lucide--shield-check]", href: "#/roles" },
],
class: "btn-primary",
onclick: () => {
Toast("Cambio de toggle");
$toggle(!$toggle());
},
},
"Lanzar Toast",
),
]);
};
const Profile = (params) => {
const miFecha = $({ start: null, end: null });
const textoInput = $(() => {
const f = miFecha;
if (!f.start) return "";
return f.end ? `${f.start} - ${f.end}` : `${f.start}...`;
});
return Div({ class: "p-4 space-y-4" }, [
H2({ class: "text-xl font-bold" }, `Perfil: ${params.id}`),
Div({ class: "pt-4" }, [
Button(
{
label: "Pólizas",
icon: "icon-[lucide--database]",
open: false,
sub: [
{ label: "Nueva Producción", icon: "icon-[lucide--users]", href: "#/users" },
{ label: "Anulaciones", icon: "icon-[lucide--shield-check]", href: "#/roles" },
],
class: "btn-sm btn-outline",
onclick: () => $.router.go("/"),
},
{
label: "Recibos",
icon: "icon-[lucide--settings]",
sub: [
{ label: "Recibos Pendientes", icon: "icon-[lucide--user-circle]", href: "#/profile" },
{ label: "Seguridad", icon: "icon-[lucide--lock]", href: "#/security" },
],
},
{
label: "Comercial",
icon: "icon-[lucide--settings]",
sub: [
{ label: "Oportunidades", icon: "icon-[lucide--user-circle]", href: "#/profile" },
{ label: "Seguimiento", icon: "icon-[lucide--user-circle]", href: "#/profile" },
],
},
{
label: "Siniestros",
icon: "icon-[lucide--settings]",
sub: [
{ label: "Nuevos Siniestros", icon: "icon-[lucide--user-circle]", href: "#/profile" },
{ label: "Siniestros en curso", icon: "icon-[lucide--lock]", href: "#/security" },
],
},
{ label: "Acerca de", icon: "icon-[lucide--info]", href: "#/about" },
"Volver",
),
]),
]);
};
/**
* Componente Principal
*/
export const App = () => {
// Estado local de la App (ejemplo: tema o usuario)
const isDark = $(false, "sigpro-theme");
// Efecto para cambiar el tema en el HTML
$(() => {
document.documentElement.setAttribute("data-theme", isDark() ? "dark" : "light");
});
const menuItems = [
{ label: "Inicio", onclick: () => $.router.go("/") },
{ label: "Mi Perfil", onclick: () => $.router.go("/profile/42") },
{ label: "Ajustes", onclick: () => Toast("Ajustes no disponibles", "alert-error") },
];
return html`
<div data-theme="${() => (isDark() ? "dark" : "light")}" class="min-h-screen bg-base-100 text-base-content transition-colors duration-300">
<header class="navbar bg-base-200 justify-between shadow-xl px-4">
<div class="flex items-center gap-2">
<button class="btn btn-ghost" @click=${() => openMenu(!openMenu())}>
<span class="icon-[lucide--menu] size-5"></span>
</button>
</div>
return Div({ class: "min-h-screen flex flex-col" }, [
Navbar({ class: "sticky top-0 z-50 bg-base-100/80 backdrop-blur border-b border-base-300" }, [
Div({ class: "flex-1" }, [
A(
{
class: "btn btn-ghost text-xl font-black tracking-tighter",
onclick: () => $.router.go("/"),
},
"SIGPRO",
),
]),
Button({ disabled: true }, "Disabled"),
${SearchBar(buscar)}
Div({ class: "flex items-center gap-2" }, [
Swap({
class: "swap-rotate",
$value: isDark,
on: "🌙",
off: "☀️",
}),
Button({ class: "btn-circle btn-ghost", icon: "👤" }),
]),
]),
<div class="flex items-center gap-2">
<button class="btn btn-ghost" @click=${() => isDark(!isDark())}>
<span class="${() => (isDark() ? "icon-[lucide--moon]" : "icon-[lucide--sun]")}"></span>
</button>
${UserMenu()}
</div>
</header>
// --- LAYOUT PRINCIPAL ---
Div({ class: "flex flex-1" }, [
// Sidebar Lateral
Aside({ class: "w-64 bg-base-200 p-4 hidden md:block" }, [Menu({ items: menuItems, class: "bg-transparent" })]),
<c-drawer .open=${openMenu} @change=${(e) => openMenu(false)}>
<c-menu cls="menu-lg" .items=${menuConfig} @select=${() => openMenu(false)}></c-menu>
</c-drawer>
// Contenido Dinámico (Router)
Main({ class: "flex-1 p-6 bg-base-100" }, [
$.router([
{ path: "/", component: Home },
{ path: "/profile/:id", component: Profile },
{
path: "*",
component: () =>
Div({ class: "text-center py-20" }, [H1({ class: "text-9xl font-bold opacity-20" }, "404"), P("La página que buscas no existe.")]),
},
]),
]),
]),
<main class="p-4 flex flex-col gap-4 mx-auto w-full">
<div class="p-4 bg-base-100 rounded-box shadow-sm">${$.router(routes)}</div>
</main>
</div>
`;
}
// --- FOOTER ---
Footer({ class: "footer footer-center p-4 bg-base-300 text-base-content text-xs" }, [P("© 2026 - Built with SigPro Engine")]),
]);
};

View File

@@ -6,7 +6,7 @@
dark --prefersdark;
include:
alert, avatar, badge, button, card, checkbox, collapse, drawer, dropdown, fab, fieldset, loading, indicator, input, kbd, label, list, menu, modal,
navbar, radio, range, select, skeleton, tab, textarea, toast, toggle, tooltip, validator, rating, mask;
navbar, radio, range, select, skeleton, tab, textarea, toast, toggle, tooltip, validator, rating, mask, swap;
}
@font-face {

View File

@@ -1,14 +1,6 @@
import App from "./App.js";
import "./app.css";
const root = document.getElementById("app");
root.appendChild(App());
if (import.meta.hot) {
import.meta.hot.accept("./App.js", (newModule) => {
if (newModule) {
root.innerHTML = "";
root.appendChild(newModule.default());
console.log("🚀 SigPro: App re-renderizada");
}
});
}
import { $ } from './sigpro.js';
import { UI } from './sigpro-ui.js';
import {App} from './App.js';
import './app.css';
UI($, 'es');
$.mount(App, '#app');

14
src/pages/about.js Normal file
View File

@@ -0,0 +1,14 @@
import { html, $ } from "sigpro";
import "@components/Button";
import "@components/Input";
import "@components/DatePicker";
import "@components/ColorPicker";
// ✅ Envuelve todo con $.page
export default $.page(() => {
return html`
<div>About</div>
`;
});

View File

@@ -13,6 +13,6 @@ export default $.page(() => {
const miColor = $("#6366f1");
return html`
<div>Hi</div>
`;
});

552
src/sigpro-ui.js Normal file
View File

@@ -0,0 +1,552 @@
/**
* SigPro UI - daisyUI v5 & Tailwind v4 Plugin
* Provides a set of reactive functional components, flow control and i18n.
*/
export const UI = ($, defaultLang = "es") => {
const ui = {};
// --- I18N CORE ---
const i18n = {
es: { close: "Cerrar", confirm: "Confirmar", cancel: "Cancelar", search: "Buscar...", loading: "Cargando..." },
en: { close: "Close", confirm: "Confirm", cancel: "Cancel", search: "Search...", loading: "Loading..." },
};
const currentLocale = $(defaultLang);
/** SET LOCALE */
ui.SetLocale = (locale) => currentLocale(locale);
/** TRANSLATE */
const tt = (key) => () => i18n[currentLocale()][key] || key;
// --- INTERNAL HELPERS ---
const val = (v) => (typeof v === "function" ? v() : v);
const joinClass = (base, extra) => {
if (typeof extra === "function") {
return () => `${base} ${extra() || ""}`.trim();
}
return `${base} ${extra || ""}`.trim();
};
// --- UTILITY FUNCTIONS ---
/** IF */
ui.If = (condition, thenValue, otherwiseValue = null) => {
return () => {
const isTrue = val(condition);
const result = isTrue ? thenValue : otherwiseValue;
if (typeof result === "function" && !(result instanceof HTMLElement)) {
return result();
}
return result;
};
};
/** FOR */
ui.For = (source, render, keyFn) => {
if (typeof keyFn !== "function") throw new Error("SigPro UI: For requires a keyFn.");
const marker = document.createTextNode("");
const container = $.html("div", { style: "display:contents" }, [marker]);
const cache = new Map();
$(() => {
const items = val(source) || [];
const newKeys = new Set();
items.forEach((item, index) => {
const key = keyFn(item, index);
newKeys.add(key);
if (cache.has(key)) {
const runtime = cache.get(key);
container.insertBefore(runtime.container, marker);
} else {
const runtime = $.view(() => {
return $.html("div", { style: "display:contents" }, [render(item, index)]);
});
cache.set(key, runtime);
container.insertBefore(runtime.container, marker);
}
});
cache.forEach((runtime, key) => {
if (!newKeys.has(key)) {
runtime.destroy();
runtime.container.remove();
cache.delete(key);
}
});
});
return container;
};
/** REQ */
ui.Request = (url, payload = null, options = {}) => {
const data = $(null),
loading = $(false),
error = $(null),
success = $(false);
let abortController = null;
const execute = async (customPayload = null) => {
const targetUrl = val(url);
if (!targetUrl) return;
if (abortController) abortController.abort();
abortController = new AbortController();
loading(true);
error(null);
success(false);
try {
const bodyData = customPayload || payload;
const res = await fetch(targetUrl, {
method: options.method || (bodyData ? "POST" : "GET"),
headers: { "Content-Type": "application/json", ...options.headers },
body: bodyData ? JSON.stringify(bodyData) : null,
signal: abortController.signal,
...options,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
let json = await res.json();
if (typeof options.transform === "function") json = options.transform(json);
data(json);
success(true);
} catch (err) {
if (err.name !== "AbortError") error(err.message);
} finally {
loading(false);
}
};
$(() => {
execute();
return () => abortController?.abort();
});
return { data, loading, error, success, reload: (p) => execute(p) };
};
/** RES */
ui.Response = (reqObj, renderFn) =>
$.html("div", { class: "res-container" }, [
ui.If(reqObj.loading, $.html("div", { class: "flex justify-center p-4" }, $.html("span", { class: "loading loading-dots text-primary" }))),
ui.If(reqObj.error, () =>
$.html("div", { role: "alert", class: "alert alert-error" }, [
$.html("span", {}, reqObj.error()),
ui.Button({ class: "btn-xs btn-ghost border-current", onclick: () => reqObj.reload() }, "Retry"),
]),
),
ui.If(reqObj.success, () => {
const current = reqObj.data();
return current !== null ? renderFn(current) : null;
}),
]);
// --- UI COMPONENTS ---
/** BUTTON */
ui.Button = (props, children) => {
const { badge, badgeClass, tooltip, icon, $loading, ...rest } = props;
const btn = $.html(
"button",
{
...rest,
class: joinClass("btn", props.$class || props.class),
$disabled: () => val($loading) || val(props.$disabled) || val(props.disabled),
},
[
() => (val($loading) ? $.html("span", { class: "loading loading-spinner" }) : null),
icon ? $.html("span", { class: "mr-1" }, icon) : null,
children,
],
);
let out = btn;
if (badge) {
out = $.html("div", { class: "indicator" }, [
$.html("span", { class: joinClass("indicator-item badge", badgeClass || "badge-secondary") }, badge),
out,
]);
}
return tooltip ? $.html("div", { class: "tooltip", "data-tip": tooltip }, out) : out;
};
/** INPUT */
ui.Input = (props) => {
const { label, tip, $value, $error, isSearch, ...rest } = props;
const inputEl = $.html("input", {
...rest,
placeholder: props.placeholder || (isSearch ? tt("search") : " "),
class: joinClass("input input-bordered w-full", props.$class || props.class),
// 1. Vinculamos el valor a la señal
$value: $value || props.value,
// 2. ACTUALIZAMOS la señal al escribir para que no se bloquee
oninput: (e) => {
if (typeof $value === "function") $value(e.target.value);
if (typeof props.oninput === "function") props.oninput(e);
},
$disabled: () => val(props.$disabled) || val(props.disabled),
});
if (!label && !tip && !$error) return inputEl;
return $.html("label", { class: "input floating-label fieldset-label flex flex-col gap-1" }, [
label ? $.html("span", {}, label) : null,
tip ? $.html("div", { class: "tooltip tooltip-right", "data-tip": tip }, $.html("span", { class: "badge badge-ghost badge-xs" }, "?")) : null,
inputEl,
() => (val($error) ? $.html("span", { class: "text-error text-xs" }, val($error)) : null),
]);
};
/** SELECT */
ui.Select = (props) => {
const { label, options, $value, ...rest } = props;
const selectEl = $.html(
"select",
{
...rest,
class: joinClass("select select-bordered w-full", props.$class || props.class),
$value: $value,
onchange: (e) => $value?.(e.target.value),
},
ui.For(
() => val(options) || [],
(opt) =>
$.html(
"option",
{
value: opt.value,
$selected: () => String(val($value)) === String(opt.value),
},
opt.label,
),
(opt) => opt.value,
),
);
if (!label) return selectEl;
return $.html("label", { class: "fieldset-label flex flex-col gap-1" }, [$.html("span", {}, label), selectEl]);
};
/** CHECKBOX */
ui.CheckBox = (props) => {
const { $value, tooltip, toggle, ...rest } = props;
const checkEl = $.html("input", {
...rest,
type: "checkbox",
class: () => (toggle() ? "toggle" : "checkbox"),
$checked: $value,
onchange: (e) => $value?.(e.target.checked),
});
const layout = $.html("label", { class: "label cursor-pointer justify-start gap-3" }, [
checkEl,
props.label ? $.html("span", { class: "label-text" }, props.label) : null,
]);
return tooltip ? $.html("div", { class: "tooltip", "data-tip": tooltip }, layout) : layout;
};
/** RADIO */
ui.Radio = (props) => {
const { label, tooltip, $value, value, ...rest } = props;
const radioEl = $.html("input", {
...rest,
type: "radio",
class: joinClass("radio", props.$class || props.class),
$checked: () => val($value) === value,
$disabled: () => val(props.$disabled) || val(props.disabled),
onclick: () => typeof $value === "function" && $value(value),
});
if (!label && !tooltip) return radioEl;
const layout = $.html("label", { class: "label cursor-pointer justify-start gap-3" }, [
radioEl,
label ? $.html("span", { class: "label-text" }, label) : null,
]);
return tooltip ? $.html("div", { class: "tooltip", "data-tip": tooltip }, layout) : layout;
};
/** RANGE */
ui.Range = (props) => {
const { label, tooltip, $value, ...rest } = props;
const rangeEl = $.html("input", {
...rest,
type: "range",
class: joinClass("range", props.$class || props.class),
$value: $value,
$disabled: () => val(props.$disabled) || val(props.disabled),
oninput: (e) => typeof $value === "function" && $value(e.target.value),
});
if (!label && !tooltip) return rangeEl;
const layout = $.html("div", { class: "flex flex-col gap-2" }, [label ? $.html("span", { class: "label-text" }, label) : null, rangeEl]);
return tooltip ? $.html("div", { class: "tooltip", "data-tip": tooltip }, layout) : layout;
};
/** MODAL */
ui.Modal = (props, children) => {
const { title, buttons, $open, ...rest } = props;
const close = () => $open(false);
return ui.If($open, () =>
$.html("dialog", { ...rest, class: "modal modal-open" }, [
$.html("div", { class: "modal-box" }, [
title ? $.html("h3", { class: "text-lg font-bold mb-4" }, title) : null,
children,
$.html("div", { class: "modal-action flex gap-2" }, [buttons, ui.Button({ onclick: close }, tt("close"))]),
]),
$.html(
"form",
{
method: "dialog",
class: "modal-backdrop",
onclick: (e) => (e.preventDefault(), close()),
},
$.html("button", "close"),
),
]),
);
};
/** DROPDOWN */
ui.Dropdown = (props, children) => {
const { label, ...rest } = props;
return $.html(
"div",
{
...rest,
class: joinClass("dropdown", props.$class || props.class),
},
[
label ? $.html("div", { tabindex: 0, role: "button", class: "btn m-1" }, label) : null,
$.html(
"div",
{
tabindex: 0,
class: "dropdown-content z-[50] p-2 shadow bg-base-100 rounded-box min-w-max",
},
children,
),
],
);
};
/** ACCORDION */
ui.Accordion = (props, children) => {
const { title, name, $open, open, ...rest } = props;
return $.html(
"div",
{
...rest,
class: joinClass("collapse collapse-arrow bg-base-200 mb-2", props.$class || props.class),
},
[
$.html("input", {
type: name ? "radio" : "checkbox",
name: name,
$checked: () => val($open) || val(open),
onchange: (e) => typeof $open === "function" && $open(e.target.checked),
}),
$.html("div", { class: "collapse-title text-xl font-medium" }, title),
$.html("div", { class: "collapse-content" }, children),
],
);
};
/** TABS */
ui.Tabs = (props) => {
const { items, ...rest } = props;
const itemsSignal = typeof items === "function" ? items : () => items || [];
return $.html("div", { ...rest, class: "flex flex-col gap-4 w-full" }, [
$.html(
"div",
{
role: "tablist",
class: joinClass("tabs tabs-lifted", props.$class || props.class),
},
ui.For(
itemsSignal,
(it) =>
$.html(
"a",
{
role: "tab",
class: () => joinClass("tab", val(it.active) && "tab-active", val(it.disabled) && "tab-disabled", it.tip && "tooltip"),
"data-tip": it.tip,
onclick: (e) => !val(it.disabled) && it.onclick?.(e),
},
it.label,
),
(t) => t.label,
),
),
() => {
const active = itemsSignal().find((it) => val(it.active));
if (!active) return null;
const content = val(active.content);
return $.html("div", { class: "p-4" }, [typeof content === "function" ? content() : content]);
},
]);
};
/** BADGE */
ui.Badge = (props, children) => $.html("span", { ...props, class: joinClass("badge", props.$class || props.class) }, children);
/** TOOLTIP */
ui.Tooltip = (props, children) =>
$.html("div", { ...props, class: joinClass("tooltip", props.$class || props.class), "data-tip": props.tip }, children);
/** NAVBAR */
ui.Navbar = (props, children) =>
$.html("div", { ...props, class: joinClass("navbar bg-base-100 shadow-sm px-4", props.$class || props.class) }, children);
/** MENU */
ui.Menu = (props) => {
const renderItems = (items) =>
ui.For(
() => items || [],
(it) =>
$.html("li", {}, [
it.children
? $.html("details", { open: it.open }, [
$.html("summary", {}, [it.icon && $.html("span", { class: "mr-2" }, it.icon), it.label]),
$.html("ul", {}, renderItems(it.children)),
])
: $.html("a", { class: () => (val(it.active) ? "active" : ""), onclick: it.onclick }, [
it.icon && $.html("span", { class: "mr-2" }, it.icon),
it.label,
]),
]),
(it, i) => it.label || i,
);
return $.html("ul", { ...props, class: joinClass("menu bg-base-200 rounded-box", props.$class || props.class) }, renderItems(props.items));
};
/** DRAWER */
ui.Drawer = (props) =>
$.html("div", { class: joinClass("drawer", props.$class || props.class) }, [
$.html("input", {
id: props.id,
type: "checkbox",
class: "drawer-toggle",
$checked: props.$open,
}),
$.html("div", { class: "drawer-content" }, props.content),
$.html("div", { class: "drawer-side" }, [
$.html("label", { for: props.id, class: "drawer-overlay", onclick: () => props.$open?.(false) }),
$.html("div", { class: "min-h-full bg-base-200 w-80" }, props.side),
]),
]);
/** FIELDSET */
ui.Fieldset = (props, children) =>
$.html("fieldset", { ...props, class: joinClass("fieldset bg-base-200 border border-base-300 p-4 rounded-lg", props.$class || props.class) }, [
ui.If(
() => props.legend,
() => $.html("legend", { class: "fieldset-legend font-bold" }, props.legend),
),
children,
]);
/** STACK */
ui.Stack = (props, children) => $.html("div", { ...props, class: joinClass("stack", props.$class || props.class) }, children);
/** STAT */
ui.Stat = (props) =>
$.html("div", { ...props, class: joinClass("stat", props.$class || props.class) }, [
props.icon && $.html("div", { class: "stat-figure text-secondary" }, props.icon),
props.label && $.html("div", { class: "stat-title" }, props.label),
$.html("div", { class: "stat-value" }, () => val(props.$value) ?? props.value),
props.desc && $.html("div", { class: "stat-desc" }, props.desc),
]);
/** SWAP */
ui.Swap = (props) =>
$.html("label", { class: joinClass("swap", props.$class || props.class) }, [
$.html("input", {
type: "checkbox",
$checked: props.$value,
onchange: (e) => props.$value?.(e.target.checked),
}),
$.html("div", { class: "swap-on" }, props.on),
$.html("div", { class: "swap-off" }, props.off),
]);
/** INDICATOR */
ui.Indicator = (props, children) =>
$.html("div", { class: joinClass("indicator", props.$class || props.class) }, [
children,
$.html("span", { class: joinClass("indicator-item badge", props.badgeClass) }, props.badge),
]);
/** TOAST */
ui.Toast = (message, type = "alert-success", duration = 3500) => {
let container = document.getElementById("sigpro-toast-container");
if (!container) {
container = $.html("div", { id: "sigpro-toast-container", class: "fixed top-0 right-0 z-[9999] p-4 flex flex-col gap-2" });
document.body.appendChild(container);
}
const runtime = $.view(() => {
const el = $.html("div", { class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0` }, [
$.html("span", message),
ui.Button({ class: "btn-xs btn-circle btn-ghost", onclick: () => remove() }, "✕"),
]);
const remove = () => {
el.classList.add("translate-x-full", "opacity-0");
setTimeout(() => {
runtime.destroy();
el.remove();
if (!container.hasChildNodes()) container.remove();
}, 300);
};
setTimeout(remove, duration);
return el;
});
container.appendChild(runtime.container);
requestAnimationFrame(() => runtime.container.firstChild.classList.remove("translate-x-10", "opacity-0"));
};
/** LOADING */
ui.Loading = (props) => {
return ui.If(props.$show, () =>
$.html("div", { class: "fixed inset-0 z-[100] flex items-center justify-center backdrop-blur-sm bg-base-100/30" }, [
$.html("span", { class: "loading loading-spinner loading-lg text-primary" }),
]),
);
};
ui.tt = tt;
Object.keys(ui).forEach((key) => {
window[key] = ui[key];
$[key] = ui[key];
});
return ui;
};

364
src/sigpro.js Normal file
View File

@@ -0,0 +1,364 @@
/**
* SigPro Core - Estabilidad Total
*/
(() => {
let activeEffect = null;
let currentOwner = null;
const effectQueue = new Set();
let isFlushing = false;
const MOUNTED_NODES = new WeakMap();
const flush = () => {
if (isFlushing) return;
isFlushing = true;
while (effectQueue.size > 0) {
const sorted = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
effectQueue.clear();
for (const eff of sorted) if (!eff._deleted) eff();
}
isFlushing = false;
};
const track = (subs) => {
if (activeEffect && !activeEffect._deleted) {
subs.add(activeEffect);
activeEffect._deps.add(subs);
}
};
const trigger = (subs) => {
for (const eff of subs) {
if (eff === activeEffect || eff._deleted) continue;
if (eff._isComputed) {
eff.markDirty();
if (eff._subs) trigger(eff._subs);
} else {
effectQueue.add(eff);
}
}
if (!isFlushing) queueMicrotask(flush);
};
const isObj = (v) => v && typeof v === "object" && !(v instanceof Node);
const PROXIES = new WeakMap();
const RAW_SUBS = new WeakMap();
const getPropSubs = (target, prop) => {
let props = RAW_SUBS.get(target);
if (!props) RAW_SUBS.set(target, (props = new Map()));
let subs = props.get(prop);
if (!subs) props.set(prop, (subs = new Set()));
return subs;
};
const $ = (initial, key) => {
if (isObj(initial) && !key && typeof initial !== "function") {
if (PROXIES.has(initial)) return PROXIES.get(initial);
const proxy = new Proxy(initial, {
get(t, p, r) {
track(getPropSubs(t, p));
const val = Reflect.get(t, p, r);
return isObj(val) ? $(val) : val;
},
set(t, p, v, r) {
const old = Reflect.get(t, p, r);
if (Object.is(old, v)) return true;
const res = Reflect.set(t, p, v, r);
trigger(getPropSubs(t, p));
if (Array.isArray(t) && p !== "length") trigger(getPropSubs(t, "length"));
return res;
},
deleteProperty(t, p) {
const res = Reflect.deleteProperty(t, p);
trigger(getPropSubs(t, p));
return res;
},
});
PROXIES.set(initial, proxy);
return proxy;
}
if (typeof initial === "function") {
const subs = new Set();
let cached,
dirty = true;
const effect = () => {
if (effect._deleted) return;
effect._cleanups.forEach((c) => c());
effect._cleanups.clear();
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
const prev = activeEffect;
activeEffect = effect;
try {
let maxD = 0;
effect._deps.forEach((s) => {
if (s._d > maxD) maxD = s._d;
});
effect.depth = maxD + 1;
subs._d = effect.depth;
const val = initial();
if (!Object.is(cached, val) || dirty) {
cached = val;
dirty = false;
trigger(subs);
}
} finally {
activeEffect = prev;
}
};
effect._isComputed = true;
effect._deps = new Set();
effect._cleanups = new Set();
effect._subs = subs;
effect.markDirty = () => (dirty = true);
effect.stop = () => {
effect._deleted = true;
effectQueue.delete(effect);
effect._cleanups.forEach((c) => c());
effect._deps.forEach((s) => s.delete(effect));
subs.clear();
};
if (currentOwner) {
currentOwner.cleanups.add(effect.stop);
effect._isComputed = false;
effect();
return () => {};
}
return () => {
if (dirty) effect();
track(subs);
return cached;
};
}
const subs = new Set();
subs._d = 0;
if (key) {
try {
const s = localStorage.getItem(key);
if (s !== null) initial = JSON.parse(s);
} catch (e) {}
}
return (...args) => {
if (args.length) {
const next = typeof args[0] === "function" ? args[0](initial) : args[0];
if (!Object.is(initial, next)) {
initial = next;
if (key)
try {
localStorage.setItem(key, JSON.stringify(initial));
} catch (e) {}
trigger(subs);
}
}
track(subs);
return initial;
};
};
const sweep = (node) => {
if (node._cleanups) {
node._cleanups.forEach((f) => f());
node._cleanups.clear();
}
node.childNodes?.forEach(sweep);
};
$.view = (fn) => {
const cleanups = new Set();
const prev = currentOwner;
const container = document.createElement("div");
container.style.display = "contents";
currentOwner = { cleanups };
try {
const res = fn({ onCleanup: (f) => cleanups.add(f) });
const process = (n) => {
if (!n) return;
if (n._isRuntime) {
cleanups.add(n.destroy);
container.appendChild(n.container);
} else if (Array.isArray(n)) n.forEach(process);
else container.appendChild(n instanceof Node ? n : document.createTextNode(String(n)));
};
process(res);
} finally {
currentOwner = prev;
}
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach((f) => f());
sweep(container);
container.remove();
},
};
};
$.html = (tag, props = {}, content = []) => {
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
content = props;
props = {};
}
const el = document.createElement(tag);
el._cleanups = new Set();
for (let [k, v] of Object.entries(props)) {
// 1. GESTIÓN DE EVENTOS (onchange, onclick...)
if (k.startsWith("on")) {
const name = k.slice(2).toLowerCase().split(".")[0];
const mods = k.slice(2).toLowerCase().split(".").slice(1);
const handler = (e) => {
if (mods.includes("prevent")) e.preventDefault();
if (mods.includes("stop")) e.stopPropagation();
v(e);
};
el.addEventListener(name, handler, { once: mods.includes("once") });
el._cleanups.add(() => el.removeEventListener(name, handler));
}
else if (k.startsWith("$")) {
const attr = k.slice(1);
const stopAttr = $(() => {
const val = typeof v === "function" ? v() : v;
if (el[attr] === val) return;
if (attr === "value" || attr === "checked") el[attr] = val;
else if (typeof val === "boolean") el.toggleAttribute(attr, val);
else val == null ? el.removeAttribute(attr) : el.setAttribute(attr, val);
});
el._cleanups.add(stopAttr);
if (typeof v === "function") {
const evt = attr === "checked" ? "change" : "input";
const h = (e) => v(e.target[attr]);
el.addEventListener(evt, h);
el._cleanups.add(() => el.removeEventListener(evt, h));
}
}
else {
if (typeof v === "function") {
const stopAttr = $(() => {
const val = v();
if (k === "class" || k === "className") el.className = val || "";
else if (typeof val === "boolean") el.toggleAttribute(k, val);
else val == null ? el.removeAttribute(k) : el.setAttribute(k, val);
});
el._cleanups.add(stopAttr);
} else {
if (k === "class" || k === "className") el.className = v || "";
else if (typeof v === "boolean") el.toggleAttribute(k, v);
else v == null ? el.removeAttribute(k) : el.setAttribute(k, v);
}
}
}
const append = (c) => {
if (Array.isArray(c)) return c.forEach(append);
if (typeof c === "function") {
const marker = document.createTextNode("");
el.appendChild(marker);
let nodes = [];
const stopList = $(() => {
const res = c();
const next = (Array.isArray(res) ? res : [res]).map((i) =>
i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? ""),
);
nodes.forEach((n) => {
sweep(n);
n.remove();
});
next.forEach((n) => marker.parentNode?.insertBefore(n, marker));
nodes = next;
});
el._cleanups.add(stopList);
} else el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ""));
};
append(content);
return el;
};
$.ignore = (fn) => {
const prev = activeEffect;
activeEffect = null;
try {
return fn();
} finally {
activeEffect = prev;
}
};
$.router = (routes) => {
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
const outlet = Div({ class: "router-outlet" });
let current = null;
// ESTE ES EL ÚNICO EFECTO: Solo observa sPath
$(() => {
const path = sPath(); // Suscripción a la URL
// 1. Limpieza total de la página anterior
if (current) current.destroy();
outlet.innerHTML = "";
// 2. Buscamos la ruta
const parts = path.split("/").filter(Boolean);
const route =
routes.find((r) => {
const rp = r.path.split("/").filter(Boolean);
return rp.length === parts.length && rp.every((p, i) => p.startsWith(":") || p === parts[i]);
}) || routes.find((r) => r.path === "*");
if (route) {
const params = {};
route.path
.split("/")
.filter(Boolean)
.forEach((p, i) => {
if (p.startsWith(":")) params[p.slice(1)] = parts[i];
});
// 3. EJECUCIÓN AISLADA
// Usamos $.ignore para que el Router NO se suscriba a las señales de Home()
// Pero el $.view permite que Home() sea reactivo internamente.
current = $.ignore(() =>
$.view(() => {
const res = route.component(params);
return typeof res === "function" ? res() : res;
}),
);
outlet.appendChild(current.container);
}
});
return outlet;
};
$.router.go = (p) => (window.location.hash = p.replace(/^#?\/?/, "#/"));
$.mount = (component, target) => {
const el = typeof target === "string" ? document.querySelector(target) : target;
if (!el) return;
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
const instance = $.view(typeof component === "function" ? component : () => component);
el.replaceChildren(instance.container);
MOUNTED_NODES.set(el, instance);
return instance;
};
const tags =
`div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(
/\s+/,
);
tags.forEach((t) => {
window[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $.html(t, p, c);
});
window.$ = $;
})();
export const { $ } = window;

View File

@@ -1,83 +0,0 @@
import fs from 'fs';
import path from 'path';
export default function sigproRouter() {
const virtualModuleId = 'virtual:sigpro-routes';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
function getFiles(dir) {
let results = [];
if (!fs.existsSync(dir)) return results;
const list = fs.readdirSync(dir);
list.forEach(file => {
const fullPath = path.resolve(dir, file);
const stat = fs.statSync(fullPath);
if (stat && stat.isDirectory()) {
results = results.concat(getFiles(fullPath));
} else if (file.endsWith('.js')) {
results.push(fullPath);
}
});
return results;
}
return {
name: 'sigpro-router',
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
},
load(id) {
if (id === resolvedVirtualModuleId) {
const pagesDir = path.resolve(process.cwd(), 'src/pages');
let files = getFiles(pagesDir);
files = files.sort((a, b) => {
const aDyn = a.includes('[');
const bDyn = b.includes('[');
if (aDyn !== bDyn) return aDyn ? 1 : -1;
return a.length - b.length;
});
let imports = '';
let routeArray = 'export const routes = [\n';
console.log('\n🚀 [SigPro Router] Mapa de rutas generado:');
files.forEach((fullPath, i) => {
const relativePath = path.relative(pagesDir, fullPath).replace(/\\/g, '/');
const fileName = relativePath.replace('.js', '');
const varName = `Page_${i}`;
let urlPath = '/' + fileName.toLowerCase();
if (urlPath.endsWith('/index')) urlPath = urlPath.replace('/index', '') || '/';
const isDynamic = urlPath.includes('[') && urlPath.includes(']');
let finalPathValue = `'${urlPath}'`;
let paramName = null;
if (isDynamic) {
// Extraemos el nombre del parámetro (ej: de [id] extraemos 'id')
const match = urlPath.match(/\[([^\]]+)\]/);
paramName = match ? match[1] : 'id';
// CORRECCIÓN: Usamos Grupos Nombrados de JS (?<nombre>...)
// Esto permite que el router haga path.match(regex).groups
const regexStr = urlPath
.replace(/\//g, '\\/')
.replace(/\[([^\]]+)\]/, '(?<$1>[^/]+)'); // Reemplaza [id] por (?<id>[^/]+)
finalPathValue = `new RegExp("^${regexStr}$")`;
}
console.log(` ${isDynamic ? '🔗' : '📄'} ${urlPath.padEnd(20)} -> ${relativePath}`);
imports += `import ${varName} from './src/pages/${relativePath}';\n`;
routeArray += ` { path: ${finalPathValue}, component: ${varName}, isDynamic: ${isDynamic}, paramName: ${paramName ? `'${paramName}'` : 'null'} },\n`;
});
routeArray += '];';
return `${imports}\n${routeArray}`;
}
}
};
}

View File

@@ -1,6 +1,6 @@
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import { sigproRouter } from 'sigpro';
// import { sigproRouter } from './router.js';
import { resolve } from "path";
import path from "node:path";
@@ -16,7 +16,7 @@ const createAgGridLib = {
};
export default defineConfig({
plugins: [ sigproRouter(),tailwindcss()],
plugins: [tailwindcss()],
base: dev ? "/absproxy/5173/" : "/",
resolve: {
alias: {