ui updated

This commit is contained in:
2026-03-25 17:35:40 +01:00
parent 60ab8e1797
commit 8f9f87a84e
2 changed files with 116 additions and 103 deletions

View File

@@ -62,7 +62,7 @@ const Profile = (params) => {
}), }),
Datepicker({ $value: miFecha, label: "Fecha", placeholder: textoInput }), Datepicker({ $value: miFecha, label: "Fecha", placeholder: textoInput }),
Datepicker({ $value: miRango, label: "Fecha", placeholder: textoInput, range: true }), Datepicker({ $value: miRango, label: "Fecha", placeholder: textoInput, range: true }),
Colorpicker({ label: "Color del tema", $value: colorFondo, placeholder: "Elige un color..." }), Colorpicker({ show:true, label: "Color del tema", $value: colorFondo }),
Input({ type: "number", label: "Number" }), Input({ type: "number", label: "Number" }),
Input({ type: "email", label: "Email" }), Input({ type: "email", label: "Email" }),
Input({ label: "Text" }), Input({ label: "Text" }),

View File

@@ -78,22 +78,17 @@ export const UI = ($, defaultLang = "es") => {
const key = keyFn(item, index); const key = keyFn(item, index);
newKeys.add(key); newKeys.add(key);
if (cache.has(key)) { let runtime = cache.get(key);
const runtime = cache.get(key); if (!runtime) {
container.insertBefore(runtime.container, marker); runtime = $.view(() => render(item, index));
} else {
const runtime = $.view(() => {
return $.html("div", { style: "display:contents" }, [render(item, index)]);
});
cache.set(key, runtime); cache.set(key, runtime);
container.insertBefore(runtime.container, marker);
} }
container.insertBefore(runtime.container, marker);
}); });
cache.forEach((runtime, key) => { cache.forEach((runtime, key) => {
if (!newKeys.has(key)) { if (!newKeys.has(key)) {
runtime.destroy(); runtime.destroy();
runtime.container.remove();
cache.delete(key); cache.delete(key);
} }
}); });
@@ -152,7 +147,7 @@ export const UI = ($, defaultLang = "es") => {
return { data, loading, error, success, reload: (p) => execute(p) }; return { data, loading, error, success, reload: (p) => execute(p) };
}; };
/** RES */ /** RESPONSE */
ui.Response = (reqObj, renderFn) => ui.Response = (reqObj, renderFn) =>
$.html("div", { class: "res-container" }, [ $.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.loading, $.html("div", { class: "flex justify-center p-4" }, $.html("span", { class: "loading loading-dots text-primary" }))),
@@ -200,7 +195,6 @@ export const UI = ($, defaultLang = "es") => {
}; };
/** INPUT */ /** INPUT */
ui.Input = (props) => { ui.Input = (props) => {
const { label, tip, $value, $error, isSearch, icon, type = "text", ...rest } = props; const { label, tip, $value, $error, isSearch, icon, type = "text", ...rest } = props;
@@ -318,11 +312,11 @@ export const UI = ($, defaultLang = "es") => {
}); });
const pick = (opt) => { const pick = (opt) => {
const value = typeof opt === "string" ? opt : opt.value; const valStr = typeof opt === "string" ? opt : opt.value;
const label = typeof opt === "string" ? opt : opt.label; const labelStr = typeof opt === "string" ? opt : opt.label;
query(label); query(labelStr);
$value?.(value); if (typeof $value === "function") $value(valStr);
onSelect?.(opt); onSelect?.(opt);
isOpen(false); isOpen(false);
@@ -331,24 +325,19 @@ export const UI = ($, defaultLang = "es") => {
const nav = (e) => { const nav = (e) => {
const items = list(); const items = list();
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
isOpen(true); isOpen(true);
cursor((i) => Math.min(i + 1, items.length - 1)); cursor(Math.min(cursor() + 1, items.length - 1));
} } else if (e.key === "ArrowUp") {
if (e.key === "ArrowUp") {
e.preventDefault(); e.preventDefault();
cursor((i) => Math.max(i - 1, 0)); cursor(Math.max(cursor() - 1, 0));
} } else if (e.key === "Enter" && cursor() >= 0) {
if (e.key === "Enter" && cursor() >= 0) {
e.preventDefault(); e.preventDefault();
pick(items[cursor()]); pick(items[cursor()]);
} else if (e.key === "Escape") {
isOpen(false);
} }
if (e.key === "Escape") isOpen(false);
}; };
return $.html("div", { class: "relative w-full" }, [ return $.html("div", { class: "relative w-full" }, [
@@ -360,13 +349,14 @@ export const UI = ($, defaultLang = "es") => {
onblur: () => setTimeout(() => isOpen(false), 150), onblur: () => setTimeout(() => isOpen(false), 150),
onkeydown: nav, onkeydown: nav,
oninput: (e) => { oninput: (e) => {
query(e.target.value); const v = e.target.value;
query(v);
if (typeof $value === "function") $value(v);
isOpen(true); isOpen(true);
cursor(-1); cursor(-1);
}, },
...rest, ...rest,
}), }),
$.html( $.html(
"ul", "ul",
{ {
@@ -390,23 +380,19 @@ export const UI = ($, defaultLang = "es") => {
]), ]),
(opt, i) => (typeof opt === "string" ? opt : opt.value) + i, (opt, i) => (typeof opt === "string" ? opt : opt.value) + i,
), ),
() => (list().length ? null : $.html("li", { class: "p-2 text-center opacity-50" }, "No results")),
() => (!list().length ? $.html("li", { class: "p-2 text-center opacity-50" }, "No results") : null),
], ],
), ),
]); ]);
}; };
/** DATEPICKER */ /** DATEPICKER */
ui.Datepicker = (props) => { ui.Datepicker = (props) => {
const { $value, range, label, placeholder, ...rest } = props; const { $value, range, label, placeholder, ...rest } = props;
const isOpen = $(false); const isOpen = $(false);
const internalDate = $(new Date()); const internalDate = $(new Date());
const hoverDate = $(null); 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 isRangeMode = () => val(range) === true;
const now = new Date(); const now = new Date();
@@ -424,17 +410,17 @@ export const UI = ($, defaultLang = "es") => {
const current = val($value); const current = val($value);
if (isRangeMode()) { if (isRangeMode()) {
// MODO RANGO
if (!current?.start || (current.start && current.end)) { if (!current?.start || (current.start && current.end)) {
$value({ start: dateStr, end: null }); if (typeof $value === "function") $value({ start: dateStr, end: null });
} else { } else {
const start = current.start; const start = current.start;
if (typeof $value === "function") {
$value(dateStr < start ? { start: dateStr, end: start } : { start, end: dateStr }); $value(dateStr < start ? { start: dateStr, end: start } : { start, end: dateStr });
}
isOpen(false); isOpen(false);
} }
} else { } else {
// MODO FECHA ÚNICA if (typeof $value === "function") $value(dateStr);
$value(dateStr);
isOpen(false); isOpen(false);
} }
}; };
@@ -442,19 +428,17 @@ export const UI = ($, defaultLang = "es") => {
const displayValue = $(() => { const displayValue = $(() => {
const v = val($value); const v = val($value);
if (!v) return ""; if (!v) return "";
// Si es un string (fecha única)
if (typeof v === "string") return v; if (typeof v === "string") return v;
// Si es un objeto (rango)
if (v.start && v.end) return `${v.start} - ${v.end}`; if (v.start && v.end) return `${v.start} - ${v.end}`;
if (v.start) return `${v.start}...`; if (v.start) return `${v.start}...`;
return ""; return "";
}); });
// ... resto del move, moveYear ...
const move = (m) => { const move = (m) => {
const d = internalDate(); const d = internalDate();
internalDate(new Date(d.getFullYear(), d.getMonth() + m, 1)); internalDate(new Date(d.getFullYear(), d.getMonth() + m, 1));
}; };
const moveYear = (y) => { const moveYear = (y) => {
const d = internalDate(); const d = internalDate();
internalDate(new Date(d.getFullYear() + y, d.getMonth(), 1)); internalDate(new Date(d.getFullYear() + y, d.getMonth(), 1));
@@ -482,7 +466,6 @@ export const UI = ($, defaultLang = "es") => {
onclick: (e) => e.stopPropagation(), 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 justify-between items-center mb-4 gap-1" }, [
$.html("div", { class: "flex gap-0.5" }, [ $.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: () => moveYear(-1) }, "<<"),
@@ -497,7 +480,6 @@ export const UI = ($, defaultLang = "es") => {
]), ]),
]), ]),
// Grid
$.html("div", { class: "grid grid-cols-7 gap-1", onmouseleave: () => hoverDate(null) }, [ $.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)), ...["L", "M", "X", "J", "V", "S", "D"].map((d) => $.html("div", { class: "text-[10px] opacity-40 font-bold text-center" }, d)),
() => { () => {
@@ -523,30 +505,31 @@ export const UI = ($, defaultLang = "es") => {
class: () => { class: () => {
const v = val($value); const v = val($value);
const h = hoverDate(); 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 isStart = typeof v === "string" ? v === dStr : v?.start === dStr;
const isEnd = v?.end === dStr; const isEnd = v?.end === dStr;
let inRange = false; let inRange = false;
// Solo calculamos sombreado si estamos en modo rango
if (isRangeMode()) { if (isRangeMode() && v?.start) {
if (v?.start && !v.end && h) { const start = v.start;
inRange = (dStr > v.start && dStr <= h) || (dStr < v.start && dStr >= h); if (!v.end && h) {
} else if (v?.start && v?.end) { inRange = (dStr > start && dStr <= h) || (dStr < start && dStr >= h);
inRange = dStr > v.start && dStr < v.end; } else if (v.end) {
inRange = dStr > start && dStr < v.end;
} }
} }
return `btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative const base = "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"} const state = 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" : ""}`; const today = dStr === todayStr ? "ring-1 ring-primary ring-inset font-black text-primary" : "";
return `${base} ${state} ${today}`;
},
onmouseenter: () => {
if (isRangeMode()) hoverDate(dStr);
}, },
onmouseenter: () => isRangeMode() && hoverDate(dStr),
onclick: () => selectDate(date), onclick: () => selectDate(date),
}, },
i.toString(), [i.toString()],
), ),
); );
} }
@@ -563,40 +546,42 @@ export const UI = ($, defaultLang = "es") => {
/** COLORPICKER */ /** COLORPICKER */
ui.Colorpicker = (props) => { ui.Colorpicker = (props) => {
const { $value, label, placeholder, ...rest } = props; const { $value, label, ...rest } = props;
const isOpen = $(false); const isOpen = $(false);
const p1 = ["#000", "#1A1A1A", "#333", "#4D4D4D", "#666", "#808080", "#B3B3B3", "#FFF"]; const palette = [
const p2 = ["#450a0a", "#7f1d1d", "#991b1b", "#b91c1c", "#dc2626", "#ef4444", "#f87171", "#fca5a5"]; ...["#000", "#1A1A1A", "#333", "#4D4D4D", "#666", "#808080", "#B3B3B3", "#FFF"],
const p3 = ["#431407", "#7c2d12", "#9a3412", "#c2410c", "#ea580c", "#f97316", "#fb923c", "#ffedd5"]; ...["#450a0a", "#7f1d1d", "#991b1b", "#b91c1c", "#dc2626", "#ef4444", "#f87171", "#fca5a5"],
const p4 = ["#713f12", "#a16207", "#ca8a04", "#eab308", "#facc15", "#fde047", "#fef08a", "#fff9c4"]; ...["#431407", "#7c2d12", "#9a3412", "#c2410c", "#ea580c", "#f97316", "#fb923c", "#ffedd5"],
const p5 = ["#064e3b", "#065f46", "#059669", "#10b981", "#34d399", "#4ade80", "#84cc16", "#d9f99d"]; ...["#713f12", "#a16207", "#ca8a04", "#eab308", "#facc15", "#fde047", "#fef08a", "#fff9c4"],
const p6 = ["#082f49", "#075985", "#0284c7", "#0ea5e9", "#38bdf8", "#7dd3fc", "#22d3ee", "#cffafe"]; ...["#064e3b", "#065f46", "#059669", "#10b981", "#34d399", "#4ade80", "#84cc16", "#d9f99d"],
const p7 = ["#1e1b4b", "#312e81", "#4338ca", "#4f46e5", "#6366f1", "#818cf8", "#a5b4fc", "#e0e7ff"]; ...["#082f49", "#075985", "#0284c7", "#0ea5e9", "#38bdf8", "#7dd3fc", "#22d3ee", "#cffafe"],
const p8 = ["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"]; ...["#1e1b4b", "#312e81", "#4338ca", "#4f46e5", "#6366f1", "#818cf8", "#a5b4fc", "#e0e7ff"],
...["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"],
const palette = [...p1, ...p2, ...p3, ...p4, ...p5, ...p6, ...p7, ...p8]; ];
const getColor = () => val($value) || "#000000"; const getColor = () => val($value) || "#000000";
return $.html("div", { class: "relative w-full" }, [ return $.html("div", { class: "relative w-fit" }, [
ui.Input({ $.html(
label, "button",
$value, {
readonly: true, type: "button",
placeholder: placeholder || "#000000", class: "btn px-3 bg-base-100 border-base-300 hover:border-primary/50 flex items-center gap-2 shadow-sm font-normal normal-case",
class: "cursor-pointer font-mono uppercase",
onclick: (e) => { onclick: (e) => {
e.stopPropagation(); e.stopPropagation();
isOpen(!isOpen()); isOpen(!isOpen());
}, },
icon: () =>
$.html("div", {
class: "size-5 rounded shadow-inner border border-base-content/10 transition-colors",
style: `background-color: ${getColor()}`,
}),
...rest, ...rest,
},
[
$.html("div", {
class: "size-5 rounded-sm shadow-inner border border-black/10 shrink-0",
style: () => `background-color: ${getColor()}`,
}), }),
label ? $.html("span", { class: "opacity-80" }, label) : null,
],
),
ui.If(isOpen, () => ui.If(isOpen, () =>
$.html( $.html(
@@ -624,7 +609,7 @@ export const UI = ($, defaultLang = "es") => {
}, },
}), }),
), ),
) ),
], ],
), ),
), ),
@@ -644,7 +629,7 @@ export const UI = ($, defaultLang = "es") => {
const checkEl = $.html("input", { const checkEl = $.html("input", {
...rest, ...rest,
type: "checkbox", type: "checkbox",
class: () => (toggle() ? "toggle" : "checkbox"), class: () => (val(toggle) ? "toggle" : "checkbox"),
$checked: $value, $checked: $value,
onchange: (e) => $value?.(e.target.checked), onchange: (e) => $value?.(e.target.checked),
}); });
@@ -709,8 +694,11 @@ export const UI = ($, defaultLang = "es") => {
$.html("dialog", { ...rest, class: "modal modal-open" }, [ $.html("dialog", { ...rest, class: "modal modal-open" }, [
$.html("div", { class: "modal-box" }, [ $.html("div", { class: "modal-box" }, [
title ? $.html("h3", { class: "text-lg font-bold mb-4" }, title) : null, title ? $.html("h3", { class: "text-lg font-bold mb-4" }, title) : null,
children, typeof children === "function" ? children() : children,
$.html("div", { class: "modal-action flex gap-2" }, [buttons, ui.Button({ onclick: close }, tt("close"))]), $.html("div", { class: "modal-action flex gap-2" }, [
...(Array.isArray(buttons) ? buttons : [buttons]).filter(Boolean),
ui.Button({ onclick: close }, tt("close")()),
]),
]), ]),
$.html( $.html(
"form", "form",
@@ -719,7 +707,7 @@ export const UI = ($, defaultLang = "es") => {
class: "modal-backdrop", class: "modal-backdrop",
onclick: (e) => (e.preventDefault(), close()), onclick: (e) => (e.preventDefault(), close()),
}, },
$.html("button", "close"), [$.html("button", {}, "close")],
), ),
]), ]),
); );
@@ -727,23 +715,31 @@ export const UI = ($, defaultLang = "es") => {
/** DROPDOWN */ /** DROPDOWN */
ui.Dropdown = (props, children) => { ui.Dropdown = (props, children) => {
const { label, ...rest } = props; const { label, icon, ...rest } = props;
return $.html( return $.html(
"div", "div",
{ {
...rest, ...rest,
class: joinClass("dropdown", props.$class || props.class), class: () => `dropdown ${val(props.$class) || props.class || ""}`,
}, },
[ [
label ? $.html("div", { tabindex: 0, role: "button", class: "btn m-1" }, label) : null,
$.html( $.html(
"div", "div",
{ {
tabindex: 0, tabindex: 0,
class: "dropdown-content z-[50] p-2 shadow bg-base-100 rounded-box min-w-max", role: "button",
class: "btn m-1 flex items-center gap-2",
}, },
children, [icon ? (typeof icon === "function" ? icon() : icon) : null, label ? (typeof label === "function" ? label() : label) : null],
),
$.html(
"ul",
{
tabindex: 0,
class: "dropdown-content z-[50] menu p-2 shadow bg-base-100 rounded-box min-w-max border border-base-300",
},
[typeof children === "function" ? children() : children],
), ),
], ],
); );
@@ -904,21 +900,35 @@ export const UI = ($, defaultLang = "es") => {
ui.Toast = (message, type = "alert-success", duration = 3500) => { ui.Toast = (message, type = "alert-success", duration = 3500) => {
let container = document.getElementById("sigpro-toast-container"); let container = document.getElementById("sigpro-toast-container");
if (!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" }); 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); document.body.appendChild(container);
} }
const runtime = $.view(() => { const runtime = $.view(() => {
const el = $.html("div", { class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0` }, [ const el = $.html(
$.html("span", message), "div",
ui.Button({ class: "btn-xs btn-circle btn-ghost", onclick: () => remove() }, "✕"), {
]); class: `alert alert-soft ${type} shadow-lg transition-all duration-300 translate-x-10 opacity-0`,
},
[
$.html("span", typeof message === "function" ? message() : message),
ui.Button(
{
class: "btn-xs btn-circle btn-ghost",
onclick: () => remove(),
},
"✕",
),
],
);
const remove = () => { const remove = () => {
el.classList.add("translate-x-full", "opacity-0"); el.classList.add("translate-x-full", "opacity-0");
setTimeout(() => { setTimeout(() => {
runtime.destroy(); runtime.destroy();
el.remove();
if (!container.hasChildNodes()) container.remove(); if (!container.hasChildNodes()) container.remove();
}, 300); }, 300);
}; };
@@ -928,7 +938,10 @@ export const UI = ($, defaultLang = "es") => {
}); });
container.appendChild(runtime.container); container.appendChild(runtime.container);
requestAnimationFrame(() => runtime.container.firstChild.classList.remove("translate-x-10", "opacity-0")); requestAnimationFrame(() => {
const el = runtime.container.firstElementChild;
if (el) el.classList.remove("translate-x-10", "opacity-0");
});
}; };
/** LOADING */ /** LOADING */