From f4213e3162f4a6a286c100b0b3f6d3d613bb5767 Mon Sep 17 00:00:00 2001 From: natxocc Date: Wed, 25 Mar 2026 03:17:28 +0100 Subject: [PATCH] Amunt! --- src/App.js | 19 ++++-- src/sigpro-ui.js | 169 ++++++++++++++++++++++++++++++++++++++++------- src/sigpro.js | 139 ++++++++++++++++++++++---------------- 3 files changed, 240 insertions(+), 87 deletions(-) diff --git a/src/App.js b/src/App.js index d0fe279..6f038ab 100644 --- a/src/App.js +++ b/src/App.js @@ -7,11 +7,9 @@ import { $ } from "./sigpro"; */ const $valor = $(""); - const $toggle = $(false); +const $toggle = $(false); // const consoleToggle = $(()=>console.log($toggle())) const Home = () => { - - const miCheck = $(false); // Creamos la seƱal return Div({ class: "prose" }, [ H1("Dashboard Principal"), @@ -43,6 +41,8 @@ const Home = () => { const Profile = (params) => { const miFecha = $({ start: null, end: null }); + const selectedFruit = $("Apple"); + const fruits = ["Apple", "Banana", "Cherry", "Dragonfruit", "Elderberry"]; const textoInput = $(() => { const f = miFecha; @@ -52,7 +52,16 @@ const Profile = (params) => { return Div({ class: "p-4 space-y-4" }, [ H2({ class: "text-xl font-bold" }, `Perfil: ${params.id}`), - + Autocomplete({ + label: "Selecciona una fruta", + $value: selectedFruit, + options: fruits, + onSelect: (val) => console.log("Seleccionado:", val), + }), + Input({type: "number", label: "Number", icon: "šŸ”"}), + Input({type: "date", label: "Date"}), + Input({type: "password", label: "Password"}), + $.html("p", {}, () => `Has elegido: ${selectedFruit()}`), Div({ class: "pt-4" }, [ Button( { @@ -72,7 +81,7 @@ export const App = () => { const isDark = $(false, "sigpro-theme"); // Efecto para cambiar el tema en el HTML - $(() => { + $$(() => { document.documentElement.setAttribute("data-theme", isDark() ? "dark" : "light"); }); diff --git a/src/sigpro-ui.js b/src/sigpro-ui.js index e443234..525fee9 100644 --- a/src/sigpro-ui.js +++ b/src/sigpro-ui.js @@ -51,7 +51,7 @@ export const UI = ($, defaultLang = "es") => { const container = $.html("div", { style: "display:contents" }, [marker]); const cache = new Map(); - $(() => { + $$(() => { const items = val(source) || []; const newKeys = new Set(); @@ -181,32 +181,54 @@ export const UI = ($, defaultLang = "es") => { }; /** INPUT */ - ui.Input = (props) => { - const { label, tip, $value, $error, isSearch, ...rest } = props; +ui.Input = (props) => { + const { label, tip, $value, $error, isSearch, icon, ...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), - }); + // Estado local para alternar visibilidad si es password + const isPassword = props.type === "password"; + const showPassword = $(false); - if (!label && !tip && !$error) return inputEl; + const inputEl = $.html("input", { + ...rest, + // El tipo cambia dinĆ”micamente si es password + type: () => (isPassword ? (showPassword() ? "text" : "password") : (props.type || "text")), + placeholder: props.placeholder || (isSearch ? tt("search")() : " "), + class: joinClass("grow order-2", props.$class || props.class), + $value: $value || props.value, + 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), + }); - 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), - ]); - }; + return $.html("label", { + class: "input input-bordered floating-label flex items-center gap-2 w-full relative" + }, [ + // 1. Icono Izquierda (opcional) + icon ? $.html("div", { class: "order-1 flex items-center opacity-50 shrink-0" }, icon) : null, + + // 2. Texto del Label + label ? $.html("span", { class: "order-0" }, label) : null, + + // 3. Input + inputEl, + + // 4. Botón Ojo (Solo si es type="password") + isPassword ? $.html("button", { + type: "button", + class: "order-3 btn btn-ghost btn-xs btn-circle opacity-50 hover:opacity-100", + onclick: (e) => { + e.preventDefault(); + showPassword(!showPassword()); + } + }, () => (showPassword() ? "šŸ™ˆ" : "šŸ‘ļø")) : null, + + // 5. Tooltip/Error + tip ? $.html("div", { class: "tooltip tooltip-right order-4", "data-tip": tip }, $.html("span", { class: "badge badge-ghost badge-xs" }, "?")) : null, + () => (val($error) ? $.html("span", { class: "text-error text-xs absolute -bottom-5 left-0" }, val($error)) : null), + ]); +}; /** SELECT */ ui.Select = (props) => { @@ -240,6 +262,105 @@ export const UI = ($, defaultLang = "es") => { return $.html("label", { class: "fieldset-label flex flex-col gap-1" }, [$.html("span", {}, label), selectEl]); }; + /** AUTOCOMPLETE */ + ui.Autocomplete = (props) => { + const { options = [], $value, onSelect, label, placeholder, ...rest } = props; + + const query = $(val($value) || ""); + const isOpen = $(false); + const highlightedIndex = $(-1); + + const filtered = $(() => { + const q = query().toLowerCase(); + const list = val(options) || []; + if (!q) return list; + return list.filter((opt) => { + const text = typeof opt === "string" ? opt : opt.label; + return text.toLowerCase().includes(q); + }); + }); + + const select = (opt) => { + const v = typeof opt === "string" ? opt : opt.value; + const l = typeof opt === "string" ? opt : opt.label; + query(l); + if (typeof $value === "function") $value(v); + onSelect?.(opt); + isOpen(false); + highlightedIndex(-1); + }; + + const onKeyDown = (e) => { + const list = filtered(); + if (e.key === "ArrowDown") { + e.preventDefault(); + isOpen(true); + highlightedIndex((prev) => Math.min(prev + 1, list.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + highlightedIndex((prev) => Math.max(prev - 1, 0)); + } else if (e.key === "Enter" && highlightedIndex() >= 0) { + e.preventDefault(); + select(list[highlightedIndex()]); + } else if (e.key === "Escape") { + isOpen(false); + } + }; + + return $.html( + "div", + { + class: "relative w-full", + }, + [ + ui.Input({ + label, + placeholder: placeholder || tt("search")(), + $value: query, + onfocus: () => isOpen(true), + onblur: () => setTimeout(() => isOpen(false), 200), + onkeydown: onKeyDown, + oninput: (e) => { + query(e.target.value); + isOpen(true); + highlightedIndex(-1); + }, + ...rest, + }), + + $.html( + "ul", + { + // AƑADIDO: w-full y box-border para que no se pase ni un pĆ­xel + class: "absolute left-0 w-80 max-w-[95vw] menu bg-base-100 rounded-box z-[100] mt-1 p-2 shadow-2xl max-h-60 overflow-y-auto border border-base-300", + style: () => (isOpen() && filtered().length > 0 ? "display: block" : "display: none"), + }, + [ + ui.For( + filtered, + (opt, i) => + $.html("li", { class: "w-full" }, [ + // li al 100% + $.html( + "a", + { + // AƑADIDO: block w-full para que el azul cubra todo el ancho + class: () => joinClass("block w-full", highlightedIndex() === i && "active bg-primary text-primary-content"), + onclick: () => select(opt), + onmouseenter: () => highlightedIndex(i), + }, + typeof opt === "string" ? opt : opt.label, + ), + ]), + (opt, i) => (typeof opt === "string" ? opt : opt.value) + i, + ), + () => (filtered().length === 0 ? $.html("li", { class: "disabled p-2 text-center opacity-50" }, "No hay resultados") : null), + ], + ), + ], + ); + }; + /** CHECKBOX */ ui.CheckBox = (props) => { const { $value, tooltip, toggle, ...rest } = props; diff --git a/src/sigpro.js b/src/sigpro.js index 592b345..48b2df5 100644 --- a/src/sigpro.js +++ b/src/sigpro.js @@ -51,21 +51,25 @@ return subs; }; - const $ = (initial, key) => { - if (isObj(initial) && !key && typeof initial !== "function") { + const $ = (initial) => { + if (initial && typeof initial === "object" && !(initial instanceof Node)) { 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; + return val && typeof val === "object" ? $(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")); + if (Array.isArray(t) && p !== "length") { + trigger(getPropSubs(t, "length")); + } return res; }, deleteProperty(t, p) { @@ -74,29 +78,26 @@ return res; }, }); + PROXIES.set(initial, proxy); return proxy; } if (typeof initial === "function") { const subs = new Set(); - let cached, - dirty = true; + let cached; + let 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; @@ -107,24 +108,22 @@ activeEffect = prev; } }; - effect._isComputed = true; + effect._deps = new Set(); - effect._cleanups = new Set(); + effect._isComputed = true; effect._subs = subs; + effect._deleted = false; effect.markDirty = () => (dirty = true); + effect.stop = () => { effect._deleted = true; - effectQueue.delete(effect); - effect._cleanups.forEach((c) => c()); effect._deps.forEach((s) => s.delete(effect)); + effect._deps.clear(); subs.clear(); }; - if (currentOwner) { - currentOwner.cleanups.add(effect.stop); - effect._isComputed = false; - effect(); - return () => {}; - } + + if (currentOwner) currentOwner.cleanups.add(effect.stop); + return () => { if (dirty) effect(); track(subs); @@ -132,31 +131,65 @@ }; } + let value = initial; 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) {} + const next = typeof args[0] === "function" ? args[0](value) : args[0]; + + if (!Object.is(value, next)) { + value = next; trigger(subs); } } track(subs); - return initial; + return value; }; }; + const $$ = (fn) => { + const effect = () => { + if (effect._deleted) return; + + effect._deps.forEach((s) => s.delete(effect)); + effect._deps.clear(); + effect._cleanups.forEach((c) => c()); + effect._cleanups.clear(); + + const prevEffect = activeEffect; + const prevOwner = currentOwner; + activeEffect = effect; + currentOwner = { cleanups: effect._cleanups }; + + effect.depth = prevEffect ? prevEffect.depth + 1 : 0; + + try { + fn(); + } finally { + activeEffect = prevEffect; + currentOwner = prevOwner; + } + }; + + effect._deps = new Set(); + effect._cleanups = new Set(); + effect._deleted = false; + + effect.stop = () => { + effect._deleted = true; + effectQueue.delete(effect); + effect._deps.forEach((s) => s.delete(effect)); + effect._deps.clear(); + effect._cleanups.forEach((c) => c()); + effect._cleanups.clear(); + }; + + if (currentOwner) currentOwner.cleanups.add(effect.stop); + effect(); + return effect.stop; + }; + const sweep = (node) => { if (node._cleanups) { node._cleanups.forEach((f) => f()); @@ -205,7 +238,6 @@ 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); @@ -216,12 +248,10 @@ }; el.addEventListener(name, handler, { once: mods.includes("once") }); el._cleanups.add(() => el.removeEventListener(name, handler)); - } - - else if (k.startsWith("$")) { + } else if (k.startsWith("$")) { const attr = k.slice(1); - const stopAttr = $(() => { + const stopAttr = $$(() => { const val = typeof v === "function" ? v() : v; if (el[attr] === val) return; @@ -237,11 +267,9 @@ el.addEventListener(evt, h); el._cleanups.add(() => el.removeEventListener(evt, h)); } - } - - else { + } else { if (typeof v === "function") { - const stopAttr = $(() => { + const stopAttr = $$(() => { const val = v(); if (k === "class" || k === "className") el.className = val || ""; else if (typeof val === "boolean") el.toggleAttribute(k, val); @@ -262,7 +290,7 @@ const marker = document.createTextNode(""); el.appendChild(marker); let nodes = []; - const stopList = $(() => { + 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 ?? ""), @@ -298,15 +326,12 @@ 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 + $$(() => { + const path = sPath(); - // 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) => { @@ -323,9 +348,6 @@ 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); @@ -360,5 +382,6 @@ window[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $.html(t, p, c); }); window.$ = $; + window.$$ = $$; })(); -export const { $ } = window; +export const { $, $$ } = window;