Amunt!
This commit is contained in:
169
src/sigpro-ui.js
169
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;
|
||||
|
||||
Reference in New Issue
Block a user