From 8f9f87a84e76f0558bef79b2b62c674580316962 Mon Sep 17 00:00:00 2001 From: natxocc Date: Wed, 25 Mar 2026 17:35:40 +0100 Subject: [PATCH] ui updated --- src/App.js | 2 +- src/sigpro-ui.js | 217 +++++++++++++++++++++++++---------------------- 2 files changed, 116 insertions(+), 103 deletions(-) diff --git a/src/App.js b/src/App.js index 150a171..54bdec7 100644 --- a/src/App.js +++ b/src/App.js @@ -62,7 +62,7 @@ const Profile = (params) => { }), Datepicker({ $value: miFecha, label: "Fecha", placeholder: textoInput }), 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: "email", label: "Email" }), Input({ label: "Text" }), diff --git a/src/sigpro-ui.js b/src/sigpro-ui.js index ef615cf..ff54140 100644 --- a/src/sigpro-ui.js +++ b/src/sigpro-ui.js @@ -78,22 +78,17 @@ export const UI = ($, defaultLang = "es") => { 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)]); - }); + let runtime = cache.get(key); + if (!runtime) { + runtime = $.view(() => render(item, index)); cache.set(key, runtime); - container.insertBefore(runtime.container, marker); } + container.insertBefore(runtime.container, marker); }); cache.forEach((runtime, key) => { if (!newKeys.has(key)) { runtime.destroy(); - runtime.container.remove(); cache.delete(key); } }); @@ -152,7 +147,7 @@ export const UI = ($, defaultLang = "es") => { return { data, loading, error, success, reload: (p) => execute(p) }; }; - /** RES */ + /** RESPONSE */ 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" }))), @@ -200,7 +195,6 @@ export const UI = ($, defaultLang = "es") => { }; /** INPUT */ - ui.Input = (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 value = typeof opt === "string" ? opt : opt.value; - const label = typeof opt === "string" ? opt : opt.label; + const valStr = typeof opt === "string" ? opt : opt.value; + const labelStr = typeof opt === "string" ? opt : opt.label; - query(label); - $value?.(value); + query(labelStr); + if (typeof $value === "function") $value(valStr); onSelect?.(opt); isOpen(false); @@ -331,24 +325,19 @@ export const UI = ($, defaultLang = "es") => { const nav = (e) => { const items = list(); - if (e.key === "ArrowDown") { e.preventDefault(); isOpen(true); - cursor((i) => Math.min(i + 1, items.length - 1)); - } - - if (e.key === "ArrowUp") { + cursor(Math.min(cursor() + 1, items.length - 1)); + } else if (e.key === "ArrowUp") { e.preventDefault(); - cursor((i) => Math.max(i - 1, 0)); - } - - if (e.key === "Enter" && cursor() >= 0) { + cursor(Math.max(cursor() - 1, 0)); + } else if (e.key === "Enter" && cursor() >= 0) { e.preventDefault(); pick(items[cursor()]); + } else if (e.key === "Escape") { + isOpen(false); } - - if (e.key === "Escape") isOpen(false); }; return $.html("div", { class: "relative w-full" }, [ @@ -360,13 +349,14 @@ export const UI = ($, defaultLang = "es") => { onblur: () => setTimeout(() => isOpen(false), 150), onkeydown: nav, oninput: (e) => { - query(e.target.value); + const v = e.target.value; + query(v); + if (typeof $value === "function") $value(v); isOpen(true); cursor(-1); }, ...rest, }), - $.html( "ul", { @@ -390,23 +380,19 @@ export const UI = ($, defaultLang = "es") => { ]), (opt, i) => (typeof opt === "string" ? opt : opt.value) + i, ), - - () => (!list().length ? $.html("li", { class: "p-2 text-center opacity-50" }, "No results") : null), + () => (list().length ? null : $.html("li", { class: "p-2 text-center opacity-50" }, "No results")), ], ), ]); }; /** 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(); @@ -424,17 +410,17 @@ export const UI = ($, defaultLang = "es") => { const current = val($value); if (isRangeMode()) { - // MODO RANGO if (!current?.start || (current.start && current.end)) { - $value({ start: dateStr, end: null }); + if (typeof $value === "function") $value({ start: dateStr, end: null }); } else { const start = current.start; - $value(dateStr < start ? { start: dateStr, end: start } : { start, end: dateStr }); + if (typeof $value === "function") { + $value(dateStr < start ? { start: dateStr, end: start } : { start, end: dateStr }); + } isOpen(false); } } else { - // MODO FECHA ÚNICA - $value(dateStr); + if (typeof $value === "function") $value(dateStr); isOpen(false); } }; @@ -442,19 +428,17 @@ export const UI = ($, defaultLang = "es") => { 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)); @@ -482,7 +466,6 @@ export const UI = ($, defaultLang = "es") => { 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) }, "<<"), @@ -497,7 +480,6 @@ export const UI = ($, defaultLang = "es") => { ]), ]), - // 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)), () => { @@ -523,30 +505,31 @@ export const UI = ($, defaultLang = "es") => { 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; + + if (isRangeMode() && v?.start) { + const start = v.start; + if (!v.end && h) { + inRange = (dStr > start && dStr <= h) || (dStr < start && dStr >= h); + } 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 - ${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 base = "btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative"; + const state = isStart || isEnd ? "btn-primary z-10" : inRange ? "bg-primary/20 border-none rounded-none" : "btn-ghost"; + 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), }, - i.toString(), + [i.toString()], ), ); } @@ -563,40 +546,42 @@ export const UI = ($, defaultLang = "es") => { /** COLORPICKER */ ui.Colorpicker = (props) => { - const { $value, label, placeholder, ...rest } = props; + const { $value, label, ...rest } = props; const isOpen = $(false); - const p1 = ["#000", "#1A1A1A", "#333", "#4D4D4D", "#666", "#808080", "#B3B3B3", "#FFF"]; - const p2 = ["#450a0a", "#7f1d1d", "#991b1b", "#b91c1c", "#dc2626", "#ef4444", "#f87171", "#fca5a5"]; - const p3 = ["#431407", "#7c2d12", "#9a3412", "#c2410c", "#ea580c", "#f97316", "#fb923c", "#ffedd5"]; - const p4 = ["#713f12", "#a16207", "#ca8a04", "#eab308", "#facc15", "#fde047", "#fef08a", "#fff9c4"]; - const p5 = ["#064e3b", "#065f46", "#059669", "#10b981", "#34d399", "#4ade80", "#84cc16", "#d9f99d"]; - const p6 = ["#082f49", "#075985", "#0284c7", "#0ea5e9", "#38bdf8", "#7dd3fc", "#22d3ee", "#cffafe"]; - const p7 = ["#1e1b4b", "#312e81", "#4338ca", "#4f46e5", "#6366f1", "#818cf8", "#a5b4fc", "#e0e7ff"]; - const p8 = ["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"]; - - const palette = [...p1, ...p2, ...p3, ...p4, ...p5, ...p6, ...p7, ...p8]; + const palette = [ + ...["#000", "#1A1A1A", "#333", "#4D4D4D", "#666", "#808080", "#B3B3B3", "#FFF"], + ...["#450a0a", "#7f1d1d", "#991b1b", "#b91c1c", "#dc2626", "#ef4444", "#f87171", "#fca5a5"], + ...["#431407", "#7c2d12", "#9a3412", "#c2410c", "#ea580c", "#f97316", "#fb923c", "#ffedd5"], + ...["#713f12", "#a16207", "#ca8a04", "#eab308", "#facc15", "#fde047", "#fef08a", "#fff9c4"], + ...["#064e3b", "#065f46", "#059669", "#10b981", "#34d399", "#4ade80", "#84cc16", "#d9f99d"], + ...["#082f49", "#075985", "#0284c7", "#0ea5e9", "#38bdf8", "#7dd3fc", "#22d3ee", "#cffafe"], + ...["#1e1b4b", "#312e81", "#4338ca", "#4f46e5", "#6366f1", "#818cf8", "#a5b4fc", "#e0e7ff"], + ...["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"], + ]; const getColor = () => val($value) || "#000000"; - return $.html("div", { class: "relative w-full" }, [ - ui.Input({ - label, - $value, - readonly: true, - placeholder: placeholder || "#000000", - class: "cursor-pointer font-mono uppercase", - onclick: (e) => { - e.stopPropagation(); - isOpen(!isOpen()); + return $.html("div", { class: "relative w-fit" }, [ + $.html( + "button", + { + type: "button", + 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", + onclick: (e) => { + e.stopPropagation(); + isOpen(!isOpen()); + }, + ...rest, }, - icon: () => + [ $.html("div", { - class: "size-5 rounded shadow-inner border border-base-content/10 transition-colors", - style: `background-color: ${getColor()}`, + class: "size-5 rounded-sm shadow-inner border border-black/10 shrink-0", + style: () => `background-color: ${getColor()}`, }), - ...rest, - }), + label ? $.html("span", { class: "opacity-80" }, label) : null, + ], + ), ui.If(isOpen, () => $.html( @@ -616,7 +601,7 @@ export const UI = ($, defaultLang = "es") => { class: () => { const active = getColor().toLowerCase() === c.toLowerCase(); return `size-6 rounded-sm cursor-pointer transition-all hover:scale-125 hover:z-10 active:scale-95 outline-none border border-black/5 - ${active ? "ring-2 ring-offset-1 ring-primary z-10 scale-110" : ""}`; + ${active ? "ring-2 ring-offset-1 ring-primary z-10 scale-110" : ""}`; }, onclick: () => { $value(c); @@ -624,7 +609,7 @@ export const UI = ($, defaultLang = "es") => { }, }), ), - ) + ), ], ), ), @@ -644,7 +629,7 @@ export const UI = ($, defaultLang = "es") => { const checkEl = $.html("input", { ...rest, type: "checkbox", - class: () => (toggle() ? "toggle" : "checkbox"), + class: () => (val(toggle) ? "toggle" : "checkbox"), $checked: $value, onchange: (e) => $value?.(e.target.checked), }); @@ -709,8 +694,11 @@ export const UI = ($, defaultLang = "es") => { $.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"))]), + typeof children === "function" ? children() : children, + $.html("div", { class: "modal-action flex gap-2" }, [ + ...(Array.isArray(buttons) ? buttons : [buttons]).filter(Boolean), + ui.Button({ onclick: close }, tt("close")()), + ]), ]), $.html( "form", @@ -719,7 +707,7 @@ export const UI = ($, defaultLang = "es") => { class: "modal-backdrop", onclick: (e) => (e.preventDefault(), close()), }, - $.html("button", "close"), + [$.html("button", {}, "close")], ), ]), ); @@ -727,23 +715,31 @@ export const UI = ($, defaultLang = "es") => { /** DROPDOWN */ ui.Dropdown = (props, children) => { - const { label, ...rest } = props; + const { label, icon, ...rest } = props; return $.html( "div", { ...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( "div", { 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) => { 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" }); + 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 el = $.html( + "div", + { + 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 = () => { el.classList.add("translate-x-full", "opacity-0"); setTimeout(() => { runtime.destroy(); - el.remove(); if (!container.hasChildNodes()) container.remove(); }, 300); }; @@ -928,7 +938,10 @@ export const UI = ($, defaultLang = "es") => { }); 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 */