Datepicker working with sigpro updated

This commit is contained in:
2026-03-25 15:47:37 +01:00
parent f4213e3162
commit ea4ea315ad
4 changed files with 414 additions and 181 deletions

View File

@@ -5,7 +5,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>svelte</title> <title>SigPro</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -40,7 +40,8 @@ const Home = () => {
}; };
const Profile = (params) => { const Profile = (params) => {
const miFecha = $({ start: null, end: null }); const miFecha = $();
const miRango = $();
const selectedFruit = $("Apple"); const selectedFruit = $("Apple");
const fruits = ["Apple", "Banana", "Cherry", "Dragonfruit", "Elderberry"]; const fruits = ["Apple", "Banana", "Cherry", "Dragonfruit", "Elderberry"];
@@ -58,7 +59,11 @@ const Profile = (params) => {
options: fruits, options: fruits,
onSelect: (val) => console.log("Seleccionado:", val), onSelect: (val) => console.log("Seleccionado:", val),
}), }),
Input({type: "number", label: "Number", icon: "🔍"}), Datepicker({ $value: miFecha, label: "Fecha", placeholder: textoInput }),
Datepicker({ $value: miRango, label: "Fecha", placeholder: textoInput, range: true }),
Input({ type: "number", label: "Number" }),
Input({ type: "email", label: "Email" }),
Input({ label: "Text" }),
Input({ type: "date", label: "Date" }), Input({ type: "date", label: "Date" }),
Input({ type: "password", label: "Password" }), Input({ type: "password", label: "Password" }),
$.html("p", {}, () => `Has elegido: ${selectedFruit()}`), $.html("p", {}, () => `Has elegido: ${selectedFruit()}`),
@@ -81,7 +86,7 @@ export const App = () => {
const isDark = $(false, "sigpro-theme"); const isDark = $(false, "sigpro-theme");
// Efecto para cambiar el tema en el HTML // Efecto para cambiar el tema en el HTML
$$(() => { $.effect(() => {
document.documentElement.setAttribute("data-theme", isDark() ? "dark" : "light"); document.documentElement.setAttribute("data-theme", isDark() ? "dark" : "light");
}); });

View File

@@ -1,3 +1,5 @@
import { $ } from "./sigpro";
/** /**
* SigPro UI - daisyUI v5 & Tailwind v4 Plugin * SigPro UI - daisyUI v5 & Tailwind v4 Plugin
* Provides a set of reactive functional components, flow control and i18n. * Provides a set of reactive functional components, flow control and i18n.
@@ -29,6 +31,23 @@ export const UI = ($, defaultLang = "es") => {
return `${base} ${extra || ""}`.trim(); return `${base} ${extra || ""}`.trim();
}; };
/** ICONS */
const iconShow =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADjSURBVDiN3dJNSgNBEAXgz4DZeAAVJ9tko2St3kaIFxAVt4KZeAD1GKKi7vQSydI/yHgALxAXU02GxniAFBR0v1ev+3V1sZSxjxtM8BM5wTX2/hNu4gFvOMI21iJ3cIwP3GMjF/dQ4RyraOMS34GPAmvjIrBeEnfwjoPGgSM8ooh8QtngB6Ep4BWnmaMqkY1LqqzmDC8tzNDK3/RHzLL9SloUYWfQIMuw3Yl8xrDBH6qbvZWALqbqBqVmlWF7GuKEDwPr5hbXcYdPnKBv/o39wL5wG7ULY1c9NGPzQRrjKrhli1/02zEjWyWMBwAAAABJRU5ErkJggg==";
const iconHide =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAEDSURBVDiN1dK/K8VhFAbwD+VLGSxKcu9guSQ/Zils/gNkuaX4BxRZDTdklYU/QAaDlEVGGwu2Kz/uVbKJzWDwfuv1+jHz1Km3c85znuf0Hv4jxnD2W8MItnCJ5xAX2MQcHsOQL+jEAapYQD9aQwxiDy+B3JKSe1DHCpqQYQ0PeMJOpDyAmyAAirjGbDRwFYcoYCZSzjGP+8B1gqXEUT2QxyPlqaRnGceNeENzUswwil1MBocbSU9DCAXUUI6K25HtIo5QSVaooitP9OEO65iIbE+HXSvBVRbeNZQSR9pxGil3o83HNw5hEbfYR0dKFki5ci+u8OrzIQ1/R8xx7ocL+9t4B0HPOVXjoptxAAAAAElFTkSuQmCC";
const iconClose =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAESSURBVDiNtdO7LoRBHAXwXyhVG5ddEq9AgbXiskS1dOJS6DyDRKLmFRCvQY9aIi6FnpDsrkuiQCWKmS/5dmW/VXCSyWTm/GfmnPzP8A8oYBc3eEMd59iKXCZW8YpDzCOHHszgAE9Ya3V4EY8YzXhgAndYaib68YxiO4kYRg290Bk3t/GAvbiuII/7uJ7CGG5RxSCGcJrceh2LEkzGwnIctTgnGMdFWtZ7IimFcrykirkmrkvokI7WVn1lcD9wpdFCKfVyYmE2xRdFC4mCY2ykCgaEfp/gTGhbX4pfx1FaQUEIyW/bWBUC1oAFIUgjGYdLWgQpwTJesC/4z6Eb00JG6lhpJzGPHVziEx/CZ9qM3N/iGy1pNoTrsd1eAAAAAElFTkSuQmCC";
const iconCalendar =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAACLSURBVDiN7dO9CQJBFEXhb38K0FwQrMNEVpuwB0NjrcYabECsQk0sQ1mTF4zIjrgmBh54MMx998AEwzOrmC5e8gJjbDHCJO7PHYI0v2JT4Ig9DljGwq5DkOZTLOCOMoIhBpknpHmFWx3ldaaUo6oTc2/ab7rl+508f8GvCC5oenTn4tM1cWg/nBNmD4fBH/Kfvt2TAAAAAElFTkSuQmCC";
const iconLock =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAWQAAAFkBqp2phgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAACQSURBVDiN7dKxDcJQDATQJ0YgXQQ1bAgDEIZBETPQwjakIjRQ8CMSyR8SiZKTrvHZd/r+JsYSNZrEI1ZR4ywzfElcJ55xwiITOECNTVDf4jDGoEEZ1Etcxxg8pmjRDiahb7BH20uKKPVUkVmL+YjQArdI+PT2bO9Pd/A34O71Rd9QeN/LAFUSckfUscWuG3oCgP8nrDH6T5AAAAAASUVORK5CYII=";
const iconAbc =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAB2AAAAdgFOeyYIAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAMRJREFUOI3t0bFKAmAUBeAPURD1HQwUTKPJEmzQoSWQcKpVfIuWdvU9WnqNhsYWBx0a2lvLSMKGbvQ7SO564HA497/3cu/92SPFAS5QDN9CftviDhZYYRpNPtH/rzATOsQT6jhCFzmc4DTJL6AX067hPiimuAr95RglzMJ/4AyyUXSMw3iEauhN6C0eUEMFAyzTFZ7xiOvwL3jbsPYSr3hPg3dB/o43SVYY+TnsPPwXztMG5SDr39dGM8kr4RKNDdPtJL4BNXEmsdKC+S4AAAAASUVORK5CYII=";
const icon123 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAB2AAAAdgFOeyYIAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAMxJREFUOI3t0bFKwlEUBvBfmmBEr1APIDZJ9AJJQyAIvkGP0C4uQruza+DUmuIc9AC9gBG4Nmpkw/8IB3Vw1w8u95zvnvPde77LEeUUV9HAF67QRA2nmMf5A+o4x3cWOsMYy8j7WMX6jaYbLBL/mAWe8RcHm1ihs8G94gVKQQzwlAouMcQo8p/Y28HdYpYFZmsi0MVdxD1MdrxsC500wijdvgtbI1AYtDbxMwkuFAZmE1uYwkkSqOIaHyHcxEU0vUXNPSqKr37fZ6xDwD9DPS0OyHjQHQAAAABJRU5ErkJggg==";
const iconMail =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdgAAAHYBTnsmCAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAC4SURBVDiNxdIxagJRFIXhLzLFBNJYaJslSEylWOhq3IorMGQ16SyjYCFiZWU5pTaDFvOUyTAZ8RHID69555577oXLf/OEGaY4R3g/4IhORHg3eOXYYvSAeRQ8OWQYYoNPvDQYnxUr7zBB1grCAv3QbIlxjXmAb7Txhq+rkFUKq9NUU8vcJiizwDtOWGEdmvTKqT+61H0GXsP7jSxpEGF/R1e3wkO0FBeVRnhTSBTneBB3yvOI4D/mAnvrIwKM5s4AAAAAAElFTkSuQmCC";
// --- UTILITY FUNCTIONS --- // --- UTILITY FUNCTIONS ---
/** IF */ /** IF */
@@ -51,7 +70,7 @@ export const UI = ($, defaultLang = "es") => {
const container = $.html("div", { style: "display:contents" }, [marker]); const container = $.html("div", { style: "display:contents" }, [marker]);
const cache = new Map(); const cache = new Map();
$$(() => { $.effect(() => {
const items = val(source) || []; const items = val(source) || [];
const newKeys = new Set(); const newKeys = new Set();
@@ -181,53 +200,75 @@ export const UI = ($, defaultLang = "es") => {
}; };
/** INPUT */ /** INPUT */
ui.Input = (props) => {
const { label, tip, $value, $error, isSearch, icon, ...rest } = props;
// Estado local para alternar visibilidad si es password ui.Input = (props) => {
const isPassword = props.type === "password"; const { label, tip, $value, $error, isSearch, icon, type = "text", ...rest } = props;
const showPassword = $(false);
const isPassword = type === "password";
const visible = $(false);
const iconsByType = {
text: iconAbc,
password: iconLock,
date: iconCalendar,
number: icon123,
email: iconMail,
};
const inputEl = $.html("input", { const inputEl = $.html("input", {
...rest, ...rest,
// El tipo cambia dinámicamente si es password type: () => (isPassword ? (visible() ? "text" : "password") : type),
type: () => (isPassword ? (showPassword() ? "text" : "password") : (props.type || "text")), placeholder: props.placeholder || label || (isSearch ? tt("search")() : " "),
placeholder: props.placeholder || (isSearch ? tt("search")() : " "), class: joinClass("grow order-2 focus:outline-none", props.$class || props.class),
class: joinClass("grow order-2", props.$class || props.class), $value: $value,
$value: $value || props.value,
oninput: (e) => { oninput: (e) => {
if (typeof $value === "function") $value(e.target.value); $value?.(e.target.value);
if (typeof props.oninput === "function") props.oninput(e); props.oninput?.(e);
}, },
$disabled: () => val(props.$disabled) || val(props.disabled), $disabled: () => val(props.$disabled) || val(props.disabled),
}); });
return $.html("label", { const leftIcon = icon ? icon : iconsByType[type] ? $.html("img", { src: iconsByType[type], class: "w-5 h-5 opacity-50", alt: type }) : null;
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 return $.html(
label ? $.html("span", { class: "order-0" }, label) : null, "label",
{
// 3. Input class: () => joinClass("input input-bordered floating-label flex items-center gap-2 w-full relative", val($error) ? "input-error" : ""),
},
[
leftIcon ? $.html("div", { class: "order-1 shrink-0" }, leftIcon) : null,
label ? $.html("span", { class: "text-base-content/60 order-0" }, label) : null,
inputEl, inputEl,
isPassword
// 4. Botón Ojo (Solo si es type="password") ? $.html(
isPassword ? $.html("button", { "button",
{
type: "button", type: "button",
class: "order-3 btn btn-ghost btn-xs btn-circle opacity-50 hover:opacity-100", class: "order-3 btn btn-ghost btn-xs btn-circle opacity-50 hover:opacity-100",
onclick: (e) => { onclick: (e) => {
e.preventDefault(); e.preventDefault();
showPassword(!showPassword()); visible(!visible());
} },
}, () => (showPassword() ? "🙈" : "👁️")) : null, },
() =>
$.html("img", {
class: "w-5 h-5",
src: visible() ? iconShow : iconHide,
}),
)
: null,
// 5. Tooltip/Error tip
tip ? $.html("div", { class: "tooltip tooltip-right order-4", "data-tip": tip }, $.html("span", { class: "badge badge-ghost badge-xs" }, "?")) : null, ? $.html(
() => (val($error) ? $.html("span", { class: "text-error text-xs absolute -bottom-5 left-0" }, val($error)) : null), "div",
]); { class: "tooltip tooltip-left order-4", "data-tip": tip },
$.html("span", { class: "badge badge-ghost badge-xs cursor-help" }, "?"),
)
: null,
() => (val($error) ? $.html("span", { class: "text-error text-[10px] absolute -bottom-5 left-2" }, val($error)) : null),
],
);
}; };
/** SELECT */ /** SELECT */
@@ -268,62 +309,60 @@ ui.Input = (props) => {
const query = $(val($value) || ""); const query = $(val($value) || "");
const isOpen = $(false); const isOpen = $(false);
const highlightedIndex = $(-1); const cursor = $(-1);
const filtered = $(() => { const list = $(() => {
const q = query().toLowerCase(); const q = query().toLowerCase();
const list = val(options) || []; const data = val(options) || [];
if (!q) return list; return q ? data.filter((o) => (typeof o === "string" ? o : o.label).toLowerCase().includes(q)) : data;
return list.filter((opt) => {
const text = typeof opt === "string" ? opt : opt.label;
return text.toLowerCase().includes(q);
});
}); });
const select = (opt) => { const pick = (opt) => {
const v = typeof opt === "string" ? opt : opt.value; const value = typeof opt === "string" ? opt : opt.value;
const l = typeof opt === "string" ? opt : opt.label; const label = typeof opt === "string" ? opt : opt.label;
query(l);
if (typeof $value === "function") $value(v); query(label);
$value?.(value);
onSelect?.(opt); onSelect?.(opt);
isOpen(false); isOpen(false);
highlightedIndex(-1); cursor(-1);
}; };
const onKeyDown = (e) => { const nav = (e) => {
const list = filtered(); const items = list();
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
isOpen(true); isOpen(true);
highlightedIndex((prev) => Math.min(prev + 1, list.length - 1)); cursor((i) => Math.min(i + 1, items.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);
} }
if (e.key === "ArrowUp") {
e.preventDefault();
cursor((i) => Math.max(i - 1, 0));
}
if (e.key === "Enter" && cursor() >= 0) {
e.preventDefault();
pick(items[cursor()]);
}
if (e.key === "Escape") isOpen(false);
}; };
return $.html( return $.html("div", { class: "relative w-full" }, [
"div",
{
class: "relative w-full",
},
[
ui.Input({ ui.Input({
label, label,
placeholder: placeholder || tt("search")(), placeholder: placeholder || tt("search")(),
$value: query, $value: query,
onfocus: () => isOpen(true), onfocus: () => isOpen(true),
onblur: () => setTimeout(() => isOpen(false), 200), onblur: () => setTimeout(() => isOpen(false), 150),
onkeydown: onKeyDown, onkeydown: nav,
oninput: (e) => { oninput: (e) => {
query(e.target.value); query(e.target.value);
isOpen(true); isOpen(true);
highlightedIndex(-1); cursor(-1);
}, },
...rest, ...rest,
}), }),
@@ -331,34 +370,195 @@ ui.Input = (props) => {
$.html( $.html(
"ul", "ul",
{ {
// AÑADIDO: w-full y box-border para que no se pase ni un píxel class: "absolute left-0 w-full menu bg-base-100 rounded-box mt-1 p-2 shadow-xl max-h-60 overflow-y-auto border border-base-300 z-50",
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() && list().length ? "display:block" : "display:none"),
style: () => (isOpen() && filtered().length > 0 ? "display: block" : "display: none"),
}, },
[ [
ui.For( ui.For(
filtered, list,
(opt, i) => (opt, i) =>
$.html("li", { class: "w-full" }, [ $.html("li", {}, [
// li al 100%
$.html( $.html(
"a", "a",
{ {
// AÑADIDO: block w-full para que el azul cubra todo el ancho class: () => `block w-full ${cursor() === i ? "active bg-primary text-primary-content" : ""}`,
class: () => joinClass("block w-full", highlightedIndex() === i && "active bg-primary text-primary-content"), onclick: () => pick(opt),
onclick: () => select(opt), onmouseenter: () => cursor(i),
onmouseenter: () => highlightedIndex(i),
}, },
typeof opt === "string" ? opt : opt.label, typeof opt === "string" ? opt : opt.label,
), ),
]), ]),
(opt, i) => (typeof opt === "string" ? opt : opt.value) + i, (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),
() => (!list().length ? $.html("li", { class: "p-2 text-center opacity-50" }, "No results") : null),
], ],
), ),
], ]);
};
/** DATEPICKER */
ui.Datepicker = (props) => {
const { $value, range, label, placeholder, ...rest } = props;
const isOpen = $(false);
const internalDate = $(new Date());
const hoverDate = $(null);
// Determinamos si es rango (si no se pasa nada, por defecto es falso para fecha única)
const isRangeMode = () => val(range) === true;
const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
const formatDate = (d) => {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const selectDate = (date) => {
const dateStr = formatDate(date);
const current = val($value);
if (isRangeMode()) {
// MODO RANGO
if (!current?.start || (current.start && current.end)) {
$value({ start: dateStr, end: null });
} else {
const start = current.start;
$value(dateStr < start ? { start: dateStr, end: start } : { start, end: dateStr });
isOpen(false);
}
} else {
// MODO FECHA ÚNICA
$value(dateStr);
isOpen(false);
}
};
const displayValue = $(() => {
const v = val($value);
if (!v) return "";
// Si es un string (fecha única)
if (typeof v === "string") return v;
// Si es un objeto (rango)
if (v.start && v.end) return `${v.start} - ${v.end}`;
if (v.start) return `${v.start}...`;
return "";
});
// ... resto del move, moveYear ...
const move = (m) => {
const d = internalDate();
internalDate(new Date(d.getFullYear(), d.getMonth() + m, 1));
};
const moveYear = (y) => {
const d = internalDate();
internalDate(new Date(d.getFullYear() + y, d.getMonth(), 1));
};
return $.html("div", { class: "relative w-full" }, [
ui.Input({
label,
placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."),
$value: displayValue,
readonly: true,
icon: $.html("img", { src: iconCalendar, class: "w-4 h-4 opacity-40" }),
onclick: (e) => {
e.stopPropagation();
isOpen(!isOpen());
},
...rest,
}),
ui.If(isOpen, () =>
$.html(
"div",
{
class: "absolute left-0 mt-2 p-4 bg-base-100 border border-base-300 shadow-2xl rounded-box z-[100] w-80 select-none",
onclick: (e) => e.stopPropagation(),
},
[
// Header (Igual que el tuyo)
$.html("div", { class: "flex justify-between items-center mb-4 gap-1" }, [
$.html("div", { class: "flex gap-0.5" }, [
$.html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, "<<"),
$.html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, "<"),
]),
$.html("span", { class: "text-xs font-bold uppercase flex-1 text-center" }, [
() => internalDate().toLocaleString("es-ES", { month: "long", year: "numeric" }),
]),
$.html("div", { class: "flex gap-0.5" }, [
$.html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, ">"),
$.html("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, ">>"),
]),
]),
// Grid
$.html("div", { class: "grid grid-cols-7 gap-1", onmouseleave: () => hoverDate(null) }, [
...["L", "M", "X", "J", "V", "S", "D"].map((d) => $.html("div", { class: "text-[10px] opacity-40 font-bold text-center" }, d)),
() => {
const d = internalDate();
const year = d.getFullYear();
const month = d.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const offset = firstDay === 0 ? 6 : firstDay - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const nodes = [];
for (let i = 0; i < offset; i++) nodes.push($.html("div"));
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const dStr = formatDate(date);
nodes.push(
$.html(
"button",
{
type: "button",
class: () => {
const v = val($value);
const h = hoverDate();
const isToday = dStr === todayStr;
// Lógica de selección según el tipo de dato
const isStart = typeof v === "string" ? v === dStr : v?.start === dStr;
const isEnd = v?.end === dStr;
let inRange = false;
// Solo calculamos sombreado si estamos en modo rango
if (isRangeMode()) {
if (v?.start && !v.end && h) {
inRange = (dStr > v.start && dStr <= h) || (dStr < v.start && dStr >= h);
} else if (v?.start && v?.end) {
inRange = dStr > v.start && dStr < v.end;
}
}
return `btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative
${isStart || isEnd ? "btn-primary z-10" : inRange ? "bg-primary/20 border-none rounded-none" : "btn-ghost"}
${isToday ? "ring-1 ring-primary ring-inset font-black text-primary" : ""}`;
},
onmouseenter: () => isRangeMode() && hoverDate(dStr),
onclick: () => selectDate(date),
},
i.toString(),
),
); );
}
return nodes;
},
]),
],
),
),
ui.If(isOpen, () => $.html("div", { class: "fixed inset-0 z-[90]", onclick: () => isOpen(false) })),
]);
}; };
/** CHECKBOX */ /** CHECKBOX */

View File

@@ -1,5 +1,5 @@
/** /**
* SigPro Core - Estabilidad Total * SigPro Core - Refactor Completo 2026
*/ */
(() => { (() => {
let activeEffect = null; let activeEffect = null;
@@ -40,8 +40,9 @@
}; };
const isObj = (v) => v && typeof v === "object" && !(v instanceof Node); const isObj = (v) => v && typeof v === "object" && !(v instanceof Node);
const PROXIES = new WeakMap();
const RAW_SUBS = new WeakMap(); const RAW_SUBS = new WeakMap();
const PROXIES = new WeakMap();
const getPropSubs = (target, prop) => { const getPropSubs = (target, prop) => {
let props = RAW_SUBS.get(target); let props = RAW_SUBS.get(target);
@@ -51,38 +52,10 @@
return subs; return subs;
}; };
// -------------------------
// --- $ = Signal puro ---
// -------------------------
const $ = (initial) => { 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 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"));
}
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") { if (typeof initial === "function") {
const subs = new Set(); const subs = new Set();
let cached; let cached;
@@ -90,13 +63,11 @@
const effect = () => { const effect = () => {
if (effect._deleted) return; if (effect._deleted) return;
effect._deps.forEach((s) => s.delete(effect)); effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear(); effect._deps.clear();
const prev = activeEffect; const prev = activeEffect;
activeEffect = effect; activeEffect = effect;
try { try {
const val = initial(); const val = initial();
if (!Object.is(cached, val) || dirty) { if (!Object.is(cached, val) || dirty) {
@@ -137,7 +108,6 @@
return (...args) => { return (...args) => {
if (args.length) { if (args.length) {
const next = typeof args[0] === "function" ? args[0](value) : args[0]; const next = typeof args[0] === "function" ? args[0](value) : args[0];
if (!Object.is(value, next)) { if (!Object.is(value, next)) {
value = next; value = next;
trigger(subs); trigger(subs);
@@ -148,10 +118,14 @@
}; };
}; };
const $$ = (fn) => { // -------------------------
// --- $.effect = efectos ---
// -------------------------
$.effect = (fn) => {
const owner = currentOwner;
const effect = () => { const effect = () => {
if (effect._deleted) return; if (effect._deleted) return;
effect._deps.forEach((s) => s.delete(effect)); effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear(); effect._deps.clear();
effect._cleanups.forEach((c) => c()); effect._cleanups.forEach((c) => c());
@@ -161,7 +135,6 @@
const prevOwner = currentOwner; const prevOwner = currentOwner;
activeEffect = effect; activeEffect = effect;
currentOwner = { cleanups: effect._cleanups }; currentOwner = { cleanups: effect._cleanups };
effect.depth = prevEffect ? prevEffect.depth + 1 : 0; effect.depth = prevEffect ? prevEffect.depth + 1 : 0;
try { try {
@@ -177,19 +150,60 @@
effect._deleted = false; effect._deleted = false;
effect.stop = () => { effect.stop = () => {
if (effect._deleted) return;
effect._deleted = true; effect._deleted = true;
effectQueue.delete(effect); effectQueue.delete(effect);
effect._deps.forEach((s) => s.delete(effect)); effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear(); effect._deps.clear();
effect._cleanups.forEach((c) => c()); effect._cleanups.forEach((c) => c());
effect._cleanups.clear(); effect._cleanups.clear();
if (owner) {
owner.cleanups.delete(effect.stop);
}
}; };
if (currentOwner) currentOwner.cleanups.add(effect.stop); if (owner) owner.cleanups.add(effect.stop);
effect(); effect();
return effect.stop; return effect.stop;
}; };
// -------------------------
// --- $.proxy = Proxy profundo ---
// -------------------------
$.proxy = (obj) => {
if (!isObj(obj)) throw new Error("$.proxy only works with objects or arrays");
if (PROXIES.has(obj)) return PROXIES.get(obj);
const proxy = new Proxy(obj, {
get(t, p, r) {
track(getPropSubs(t, p));
const val = Reflect.get(t, p, r);
return val && typeof val === "object" ? $.proxy(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(obj, proxy);
return proxy;
};
// -------------------------
// --- Sweep nodos y limpieza ---
// -------------------------
const sweep = (node) => { const sweep = (node) => {
if (node._cleanups) { if (node._cleanups) {
node._cleanups.forEach((f) => f()); node._cleanups.forEach((f) => f());
@@ -198,6 +212,9 @@
node.childNodes?.forEach(sweep); node.childNodes?.forEach(sweep);
}; };
// -------------------------
// --- Vistas reactivas ---
// -------------------------
$.view = (fn) => { $.view = (fn) => {
const cleanups = new Set(); const cleanups = new Set();
const prev = currentOwner; const prev = currentOwner;
@@ -229,6 +246,9 @@
}; };
}; };
// -------------------------
// --- HTML con bindings ---
// -------------------------
$.html = (tag, props = {}, content = []) => { $.html = (tag, props = {}, content = []) => {
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") { if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
content = props; content = props;
@@ -250,11 +270,9 @@
el._cleanups.add(() => el.removeEventListener(name, handler)); el._cleanups.add(() => el.removeEventListener(name, handler));
} else if (k.startsWith("$")) { } else if (k.startsWith("$")) {
const attr = k.slice(1); const attr = k.slice(1);
const stopAttr = $.effect(() => {
const stopAttr = $$(() => {
const val = typeof v === "function" ? v() : v; const val = typeof v === "function" ? v() : v;
if (el[attr] === val) return; if (el[attr] === val) return;
if (attr === "value" || attr === "checked") el[attr] = val; if (attr === "value" || attr === "checked") el[attr] = val;
else if (typeof val === "boolean") el.toggleAttribute(attr, val); else if (typeof val === "boolean") el.toggleAttribute(attr, val);
else val == null ? el.removeAttribute(attr) : el.setAttribute(attr, val); else val == null ? el.removeAttribute(attr) : el.setAttribute(attr, val);
@@ -263,13 +281,12 @@
if (typeof v === "function") { if (typeof v === "function") {
const evt = attr === "checked" ? "change" : "input"; const evt = attr === "checked" ? "change" : "input";
const h = (e) => v(e.target[attr]); const handler = (e) => v(e.target[attr]);
el.addEventListener(evt, h); el.addEventListener(evt, handler);
el._cleanups.add(() => el.removeEventListener(evt, h)); el._cleanups.add(() => el.removeEventListener(evt, handler));
} }
} else { } else if (typeof v === "function") {
if (typeof v === "function") { const stopAttr = $.effect(() => {
const stopAttr = $$(() => {
const val = v(); const val = v();
if (k === "class" || k === "className") el.className = val || ""; if (k === "class" || k === "className") el.className = val || "";
else if (typeof val === "boolean") el.toggleAttribute(k, val); else if (typeof val === "boolean") el.toggleAttribute(k, val);
@@ -282,7 +299,6 @@
else v == null ? el.removeAttribute(k) : el.setAttribute(k, v); else v == null ? el.removeAttribute(k) : el.setAttribute(k, v);
} }
} }
}
const append = (c) => { const append = (c) => {
if (Array.isArray(c)) return c.forEach(append); if (Array.isArray(c)) return c.forEach(append);
@@ -290,7 +306,7 @@
const marker = document.createTextNode(""); const marker = document.createTextNode("");
el.appendChild(marker); el.appendChild(marker);
let nodes = []; let nodes = [];
const stopList = $$(() => { const stopList = $.effect(() => {
const res = c(); const res = c();
const next = (Array.isArray(res) ? res : [res]).map((i) => const next = (Array.isArray(res) ? res : [res]).map((i) =>
i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? ""), i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? ""),
@@ -309,6 +325,9 @@
return el; return el;
}; };
// -------------------------
// --- Ignorar reactividad temporal ---
// -------------------------
$.ignore = (fn) => { $.ignore = (fn) => {
const prev = activeEffect; const prev = activeEffect;
activeEffect = null; activeEffect = null;
@@ -319,6 +338,9 @@
} }
}; };
// -------------------------
// --- Router simple ---
// -------------------------
$.router = (routes) => { $.router = (routes) => {
const sPath = $(window.location.hash.replace(/^#/, "") || "/"); const sPath = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/")); window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
@@ -326,9 +348,8 @@
const outlet = Div({ class: "router-outlet" }); const outlet = Div({ class: "router-outlet" });
let current = null; let current = null;
$$(() => { $.effect(() => {
const path = sPath(); const path = sPath();
if (current) current.destroy(); if (current) current.destroy();
outlet.innerHTML = ""; outlet.innerHTML = "";
@@ -364,6 +385,9 @@
$.router.go = (p) => (window.location.hash = p.replace(/^#?\/?/, "#/")); $.router.go = (p) => (window.location.hash = p.replace(/^#?\/?/, "#/"));
// -------------------------
// --- Montaje de componentes ---
// -------------------------
$.mount = (component, target) => { $.mount = (component, target) => {
const el = typeof target === "string" ? document.querySelector(target) : target; const el = typeof target === "string" ? document.querySelector(target) : target;
if (!el) return; if (!el) return;
@@ -374,14 +398,18 @@
return instance; return instance;
}; };
// -------------------------
// --- Shortcuts para tags ---
// -------------------------
const tags = 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( `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+/, /\s+/,
); );
tags.forEach((t) => { tags.forEach((t) => {
window[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $.html(t, p, c); window[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $.html(t, p, c);
}); });
window.$ = $; window.$ = $;
window.$$ = $$;
})(); })();
export const { $, $$ } = window; export const { $ } = window;