Amunt!
This commit is contained in:
19
src/App.js
19
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");
|
||||
});
|
||||
|
||||
|
||||
147
src/sigpro-ui.js
147
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,16 +181,20 @@ 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;
|
||||
|
||||
// Estado local para alternar visibilidad si es password
|
||||
const isPassword = props.type === "password";
|
||||
const showPassword = $(false);
|
||||
|
||||
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
|
||||
// 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,
|
||||
// 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);
|
||||
@@ -198,15 +202,33 @@ export const UI = ($, defaultLang = "es") => {
|
||||
$disabled: () => val(props.$disabled) || val(props.disabled),
|
||||
});
|
||||
|
||||
if (!label && !tip && !$error) return inputEl;
|
||||
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,
|
||||
|
||||
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,
|
||||
// 2. Texto del Label
|
||||
label ? $.html("span", { class: "order-0" }, label) : null,
|
||||
|
||||
// 3. Input
|
||||
inputEl,
|
||||
() => (val($error) ? $.html("span", { class: "text-error text-xs" }, val($error)) : null),
|
||||
|
||||
// 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;
|
||||
|
||||
139
src/sigpro.js
139
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;
|
||||
|
||||
Reference in New Issue
Block a user