From df8bd891a27961e102eb6ef356e3892201c26669 Mon Sep 17 00:00:00 2001 From: natxocc Date: Wed, 25 Mar 2026 02:03:59 +0100 Subject: [PATCH] UUUUPPPPP work --- bun.lock | 3 - {src/components => components_}/AgGrid.js | 0 {src/components => components_}/Button.js | 0 {src/components => components_}/Card.js | 0 {src/components => components_}/Checkbox.js | 0 .../components => components_}/ColorPicker.js | 0 {src/components => components_}/DatePicker.js | 0 {src/components => components_}/Dialog.js | 0 {src/components => components_}/Drawer.js | 0 {src/components => components_}/Dropdown.js | 0 {src/components => components_}/Fab.js | 0 {src/components => components_}/Input.js | 0 {src/components => components_}/InputClear.js | 0 {src/components => components_}/InputView.js | 0 {src/components => components_}/Loading.js | 0 {src/components => components_}/Menu.js | 0 {src/components => components_}/Radio.js | 0 {src/components => components_}/Range.js | 0 {src/components => components_}/Rating.js | 0 {src/components => components_}/Tab.js | 0 {src/components => components_}/Toast.js | 0 package.json | 3 +- router.js | 63 ++ src/App.js | 243 ++++---- src/app.css | 2 +- src/main.js | 20 +- src/pages/about.js | 14 + src/pages/index.js | 2 +- src/sigpro-ui.js | 552 ++++++++++++++++++ src/sigpro.js | 364 ++++++++++++ vite-plugin-sigpro.js | 83 --- vite.config.js | 10 +- 32 files changed, 1126 insertions(+), 233 deletions(-) rename {src/components => components_}/AgGrid.js (100%) rename {src/components => components_}/Button.js (100%) rename {src/components => components_}/Card.js (100%) rename {src/components => components_}/Checkbox.js (100%) rename {src/components => components_}/ColorPicker.js (100%) rename {src/components => components_}/DatePicker.js (100%) rename {src/components => components_}/Dialog.js (100%) rename {src/components => components_}/Drawer.js (100%) rename {src/components => components_}/Dropdown.js (100%) rename {src/components => components_}/Fab.js (100%) rename {src/components => components_}/Input.js (100%) rename {src/components => components_}/InputClear.js (100%) rename {src/components => components_}/InputView.js (100%) rename {src/components => components_}/Loading.js (100%) rename {src/components => components_}/Menu.js (100%) rename {src/components => components_}/Radio.js (100%) rename {src/components => components_}/Range.js (100%) rename {src/components => components_}/Rating.js (100%) rename {src/components => components_}/Tab.js (100%) rename {src/components => components_}/Toast.js (100%) create mode 100644 router.js create mode 100644 src/pages/about.js create mode 100644 src/sigpro-ui.js create mode 100644 src/sigpro.js delete mode 100644 vite-plugin-sigpro.js diff --git a/bun.lock b/bun.lock index 2a3bb19..bf9c5a5 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/src/components/AgGrid.js b/components_/AgGrid.js similarity index 100% rename from src/components/AgGrid.js rename to components_/AgGrid.js diff --git a/src/components/Button.js b/components_/Button.js similarity index 100% rename from src/components/Button.js rename to components_/Button.js diff --git a/src/components/Card.js b/components_/Card.js similarity index 100% rename from src/components/Card.js rename to components_/Card.js diff --git a/src/components/Checkbox.js b/components_/Checkbox.js similarity index 100% rename from src/components/Checkbox.js rename to components_/Checkbox.js diff --git a/src/components/ColorPicker.js b/components_/ColorPicker.js similarity index 100% rename from src/components/ColorPicker.js rename to components_/ColorPicker.js diff --git a/src/components/DatePicker.js b/components_/DatePicker.js similarity index 100% rename from src/components/DatePicker.js rename to components_/DatePicker.js diff --git a/src/components/Dialog.js b/components_/Dialog.js similarity index 100% rename from src/components/Dialog.js rename to components_/Dialog.js diff --git a/src/components/Drawer.js b/components_/Drawer.js similarity index 100% rename from src/components/Drawer.js rename to components_/Drawer.js diff --git a/src/components/Dropdown.js b/components_/Dropdown.js similarity index 100% rename from src/components/Dropdown.js rename to components_/Dropdown.js diff --git a/src/components/Fab.js b/components_/Fab.js similarity index 100% rename from src/components/Fab.js rename to components_/Fab.js diff --git a/src/components/Input.js b/components_/Input.js similarity index 100% rename from src/components/Input.js rename to components_/Input.js diff --git a/src/components/InputClear.js b/components_/InputClear.js similarity index 100% rename from src/components/InputClear.js rename to components_/InputClear.js diff --git a/src/components/InputView.js b/components_/InputView.js similarity index 100% rename from src/components/InputView.js rename to components_/InputView.js diff --git a/src/components/Loading.js b/components_/Loading.js similarity index 100% rename from src/components/Loading.js rename to components_/Loading.js diff --git a/src/components/Menu.js b/components_/Menu.js similarity index 100% rename from src/components/Menu.js rename to components_/Menu.js diff --git a/src/components/Radio.js b/components_/Radio.js similarity index 100% rename from src/components/Radio.js rename to components_/Radio.js diff --git a/src/components/Range.js b/components_/Range.js similarity index 100% rename from src/components/Range.js rename to components_/Range.js diff --git a/src/components/Rating.js b/components_/Rating.js similarity index 100% rename from src/components/Rating.js rename to components_/Rating.js diff --git a/src/components/Tab.js b/components_/Tab.js similarity index 100% rename from src/components/Tab.js rename to components_/Tab.js diff --git a/src/components/Toast.js b/components_/Toast.js similarity index 100% rename from src/components/Toast.js rename to components_/Toast.js diff --git a/package.json b/package.json index 5b5813b..ce52800 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/router.js b/router.js new file mode 100644 index 0000000..7d6dcc1 --- /dev/null +++ b/router.js @@ -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}];`; + } + }; +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index d4622e7..d0fe279 100644 --- a/src/App.js +++ b/src/App.js @@ -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"; -// --- COMPONENTE: BUSCADOR --- -const SearchBar = (buscar) => html` - -`; +/** + * Vistas de la aplicación (pueden ir en archivos separados luego) + */ -// --- COMPONENTE: MENU USUARIO --- -const UserMenu = () => html` - -`; +const $valor = $(""); + const $toggle = $(false); +// const consoleToggle = $(()=>console.log($toggle())) +const Home = () => { + + + 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( + { + class: "btn-primary", + onclick: () => { + Toast("Cambio de toggle"); + $toggle(!$toggle()); + }, + }, + "Lanzar Toast", + ), + ]); +}; -export default function App() { - const openMenu = $(false); - const buscar = $(""); +const Profile = (params) => { + const miFecha = $({ start: null, end: null }); - // 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 textoInput = $(() => { + const f = miFecha; + if (!f.start) return ""; + return f.end ? `${f.start} - ${f.end}` : `${f.start}...`; + }); - { - 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" }, - ], - }, - { - 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" }, - ], - }, - { - 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" }, + return Div({ class: "p-4 space-y-4" }, [ + H2({ class: "text-xl font-bold" }, `Perfil: ${params.id}`), + + Div({ class: "pt-4" }, [ + Button( + { + class: "btn-sm btn-outline", + onclick: () => $.router.go("/"), + }, + "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` -
- + // --- 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" })]), - openMenu(false)}> - openMenu(false)}> - + // 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.")]), + }, + ]), + ]), + ]), -
-
${$.router(routes)}
-
-
- `; -} + // --- FOOTER --- + Footer({ class: "footer footer-center p-4 bg-base-300 text-base-content text-xs" }, [P("© 2026 - Built with SigPro Engine")]), + ]); +}; diff --git a/src/app.css b/src/app.css index 5ae75e7..a312354 100644 --- a/src/app.css +++ b/src/app.css @@ -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 { diff --git a/src/main.js b/src/main.js index cadca44..17c5a45 100644 --- a/src/main.js +++ b/src/main.js @@ -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"); - } - }); -} \ No newline at end of file +import { $ } from './sigpro.js'; +import { UI } from './sigpro-ui.js'; +import {App} from './App.js'; +import './app.css'; +UI($, 'es'); +$.mount(App, '#app'); \ No newline at end of file diff --git a/src/pages/about.js b/src/pages/about.js new file mode 100644 index 0000000..9aaa436 --- /dev/null +++ b/src/pages/about.js @@ -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` +
About
+ `; +}); diff --git a/src/pages/index.js b/src/pages/index.js index 2f902bc..288f41e 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -13,6 +13,6 @@ export default $.page(() => { const miColor = $("#6366f1"); return html` - +
Hi
`; }); diff --git a/src/sigpro-ui.js b/src/sigpro-ui.js new file mode 100644 index 0000000..e443234 --- /dev/null +++ b/src/sigpro-ui.js @@ -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; +}; diff --git a/src/sigpro.js b/src/sigpro.js new file mode 100644 index 0000000..592b345 --- /dev/null +++ b/src/sigpro.js @@ -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; diff --git a/vite-plugin-sigpro.js b/vite-plugin-sigpro.js deleted file mode 100644 index 8e6f534..0000000 --- a/vite-plugin-sigpro.js +++ /dev/null @@ -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 (?...) - // Esto permite que el router haga path.match(regex).groups - const regexStr = urlPath - .replace(/\//g, '\\/') - .replace(/\[([^\]]+)\]/, '(?<$1>[^/]+)'); // Reemplaza [id] por (?[^/]+) - - 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}`; - } - } - }; -} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 3709400..2892800 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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: { @@ -39,9 +39,9 @@ export default defineConfig({ assetFileNames: (assetInfo) => { const name = assetInfo.name ?? ""; if (name.endsWith(".css")) { - return "assets/css/[name]-[hash][extname]"; - } - return "assets/[name]-[hash][extname]"; + return "assets/css/[name]-[hash][extname]"; + } + return "assets/[name]-[hash][extname]"; }, // 4. Estrategia de separación manual (Manual Chunks)