Update Sigpro

This commit is contained in:
2026-03-26 17:06:17 +01:00
parent b5da486820
commit 1f2229ddda
6 changed files with 224 additions and 182 deletions

View File

@@ -133,16 +133,16 @@ export const UI = ($, defaultLang = "es") => {
/** BUTTON */ /** BUTTON */
ui.Button = (props, children) => { ui.Button = (props, children) => {
const { badge, badgeClass, tooltip, icon, $loading, ...rest } = props; const { badge, badgeClass, tooltip, icon, loading, ...rest } = props;
const btn = $.html( const btn = $.html(
"button", "button",
{ {
...rest, ...rest,
class: joinClass("btn", props.$class || props.class), class: joinClass("btn", props.class || props.class),
$disabled: () => val($loading) || val(props.$disabled) || val(props.disabled), disabled: () => val(loading) || val(props.disabled) || val(props.disabled),
}, },
[ [
() => (val($loading) ? $.html("span", { class: "loading loading-spinner" }) : null), () => (val(loading) ? $.html("span", { class: "loading loading-spinner" }) : null),
icon ? $.html("span", { class: "mr-1" }, icon) : null, icon ? $.html("span", { class: "mr-1" }, icon) : null,
children, children,
], ],
@@ -159,8 +159,7 @@ 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;
const isPassword = type === "password"; const isPassword = type === "password";
const visible = $(false); const visible = $(false);
@@ -176,13 +175,10 @@ export const UI = ($, defaultLang = "es") => {
...rest, ...rest,
type: () => (isPassword ? (visible() ? "text" : "password") : type), type: () => (isPassword ? (visible() ? "text" : "password") : type),
placeholder: props.placeholder || label || (isSearch ? tt("search")() : " "), placeholder: props.placeholder || label || (isSearch ? tt("search")() : " "),
class: joinClass("grow order-2 focus:outline-none", props.$class || props.class), class: joinClass("grow order-2 focus:outline-none", props.class),
$value: $value, value: value,
oninput: (e) => { oninput: (e) => props.oninput?.(e),
$value?.(e.target.value); disabled: () => val(props.disabled),
props.oninput?.(e);
},
$disabled: () => val(props.$disabled) || val(props.disabled),
}); });
const leftIcon = icon ? icon : iconsByType[type] ? $.html("img", { src: iconsByType[type], class: "w-5 h-5 opacity-50", alt: type }) : null; const leftIcon = icon ? icon : iconsByType[type] ? $.html("img", { src: iconsByType[type], class: "w-5 h-5 opacity-50", alt: type }) : null;
@@ -190,7 +186,7 @@ export const UI = ($, defaultLang = "es") => {
return $.html( return $.html(
"label", "label",
{ {
class: () => joinClass("input input-bordered floating-label flex items-center gap-2 w-full relative", val($error) ? "input-error" : ""), 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, leftIcon ? $.html("div", { class: "order-1 shrink-0" }, leftIcon) : null,
@@ -198,47 +194,44 @@ export const UI = ($, defaultLang = "es") => {
inputEl, inputEl,
isPassword isPassword
? $.html( ? $.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();
visible(!visible()); visible(!visible());
},
}, },
() => },
$.html("img", { () =>
class: "w-5 h-5", $.html("img", {
src: visible() ? iconShow : iconHide, class: "w-5 h-5",
}), src: visible() ? iconShow : iconHide,
) }),
)
: null, : null,
tip tip
? $.html( ? $.html(
"div", "div",
{ class: "tooltip tooltip-left order-4", "data-tip": tip }, { class: "tooltip tooltip-left order-4", "data-tip": tip },
$.html("span", { class: "badge badge-ghost badge-xs cursor-help" }, "?"), $.html("span", { class: "badge badge-ghost badge-xs cursor-help" }, "?"),
) )
: null, : null,
() => (val(error) ? $.html("span", { class: "text-error text-[10px] absolute -bottom-5 left-2" }, val(error)) : null),
() => (val($error) ? $.html("span", { class: "text-error text-[10px] absolute -bottom-5 left-2" }, val($error)) : null),
], ],
); );
}; };
/** SELECT */ /** SELECT */
ui.Select = (props) => { ui.Select = (props) => {
const { label, options, $value, ...rest } = props; const { label, options, value, ...rest } = props;
const selectEl = $.html( const selectEl = $.html(
"select", "select",
{ {
...rest, ...rest,
class: joinClass("select select-bordered w-full", props.$class || props.class), class: joinClass("select select-bordered w-full", props.class || props.class),
$value: $value, value: value
onchange: (e) => $value?.(e.target.value),
}, },
$.for( $.for(
() => val(options) || [], () => val(options) || [],
@@ -247,7 +240,7 @@ export const UI = ($, defaultLang = "es") => {
"option", "option",
{ {
value: opt.value, value: opt.value,
$selected: () => String(val($value)) === String(opt.value), $selected: () => String(val(value)) === String(opt.value),
}, },
opt.label, opt.label,
), ),
@@ -262,9 +255,9 @@ export const UI = ($, defaultLang = "es") => {
/** AUTOCOMPLETE */ /** AUTOCOMPLETE */
ui.Autocomplete = (props) => { ui.Autocomplete = (props) => {
const { options = [], $value, onSelect, label, placeholder, ...rest } = props; const { options = [], value, onSelect, label, placeholder, ...rest } = props;
const query = $(val($value) || ""); const query = $(val(value) || "");
const isOpen = $(false); const isOpen = $(false);
const cursor = $(-1); const cursor = $(-1);
@@ -279,7 +272,7 @@ export const UI = ($, defaultLang = "es") => {
const labelStr = typeof opt === "string" ? opt : opt.label; const labelStr = typeof opt === "string" ? opt : opt.label;
query(labelStr); query(labelStr);
if (typeof $value === "function") $value(valStr); if (typeof value === "function") value(valStr);
onSelect?.(opt); onSelect?.(opt);
isOpen(false); isOpen(false);
@@ -307,14 +300,14 @@ export const UI = ($, defaultLang = "es") => {
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), 150), onblur: () => setTimeout(() => isOpen(false), 150),
onkeydown: nav, onkeydown: nav,
oninput: (e) => { oninput: (e) => {
const v = e.target.value; const v = e.target.value;
query(v); query(v);
if (typeof $value === "function") $value(v); if (typeof value === "function") value(v);
isOpen(true); isOpen(true);
cursor(-1); cursor(-1);
}, },
@@ -351,7 +344,7 @@ export const UI = ($, defaultLang = "es") => {
/** 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());
@@ -370,26 +363,26 @@ export const UI = ($, defaultLang = "es") => {
const selectDate = (date) => { const selectDate = (date) => {
const dateStr = formatDate(date); const dateStr = formatDate(date);
const current = val($value); const current = val(value);
if (isRangeMode()) { if (isRangeMode()) {
if (!current?.start || (current.start && current.end)) { if (!current?.start || (current.start && current.end)) {
if (typeof $value === "function") $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") { 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 {
if (typeof $value === "function") $value(dateStr); if (typeof value === "function") value(dateStr);
isOpen(false); isOpen(false);
} }
}; };
const displayValue = $(() => { const displayValue = $(() => {
const v = val($value); const v = val(value);
if (!v) return ""; if (!v) return "";
if (typeof v === "string") return v; if (typeof v === "string") return v;
if (v.start && v.end) return `${v.start} - ${v.end}`; if (v.start && v.end) return `${v.start} - ${v.end}`;
@@ -411,7 +404,7 @@ export const UI = ($, defaultLang = "es") => {
ui.Input({ ui.Input({
label, label,
placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."), placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."),
$value: displayValue, value: displayValue,
readonly: true, readonly: true,
icon: $.html("img", { src: iconCalendar, class: "opacity-40" }), icon: $.html("img", { src: iconCalendar, class: "opacity-40" }),
onclick: (e) => { onclick: (e) => {
@@ -482,7 +475,7 @@ export const UI = ($, defaultLang = "es") => {
{ {
type: "button", type: "button",
class: () => { class: () => {
const v = val($value); const v = val(value);
const h = hoverDate(); const h = hoverDate();
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;
@@ -525,7 +518,7 @@ export const UI = ($, defaultLang = "es") => {
/** COLORPICKER */ /** COLORPICKER */
ui.Colorpicker = (props) => { ui.Colorpicker = (props) => {
const { $value, label, ...rest } = props; const { value, label, ...rest } = props;
const isOpen = $(false); const isOpen = $(false);
const palette = [ const palette = [
@@ -539,7 +532,7 @@ export const UI = ($, defaultLang = "es") => {
...["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"], ...["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"],
]; ];
const getColor = () => val($value) || "#000000"; const getColor = () => val(value) || "#000000";
return $.html("div", { class: "relative w-fit" }, [ return $.html("div", { class: "relative w-fit" }, [
$.html( $.html(
@@ -583,7 +576,7 @@ export const UI = ($, defaultLang = "es") => {
${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: () => { onclick: () => {
$value(c); value(c);
isOpen(false); isOpen(false);
}, },
}), }),
@@ -604,13 +597,12 @@ export const UI = ($, defaultLang = "es") => {
/** CHECKBOX */ /** CHECKBOX */
ui.CheckBox = (props) => { ui.CheckBox = (props) => {
const { $value, tooltip, toggle, ...rest } = props; const { value, tooltip, toggle, ...rest } = props;
const checkEl = $.html("input", { const checkEl = $.html("input", {
...rest, ...rest,
type: "checkbox", type: "checkbox",
class: () => (val(toggle) ? "toggle" : "checkbox"), class: () => (val(toggle) ? "toggle" : "checkbox"),
$checked: $value, checked: value
onchange: (e) => $value?.(e.target.checked),
}); });
const layout = $.html("label", { class: "label cursor-pointer justify-start gap-3" }, [ const layout = $.html("label", { class: "label cursor-pointer justify-start gap-3" }, [
@@ -623,15 +615,15 @@ export const UI = ($, defaultLang = "es") => {
/** RADIO */ /** RADIO */
ui.Radio = (props) => { ui.Radio = (props) => {
const { label, tooltip, $value, value, ...rest } = props; const { label, tooltip, value, ...rest } = props;
const radioEl = $.html("input", { const radioEl = $.html("input", {
...rest, ...rest,
type: "radio", type: "radio",
class: joinClass("radio", props.$class || props.class), class: joinClass("radio", props.class || props.class),
$checked: () => val($value) === value, checked: () => val(value) === value,
$disabled: () => val(props.$disabled) || val(props.disabled), disabled: () => val(props.disabled) || val(props.disabled),
onclick: () => typeof $value === "function" && $value(value), onclick: () => typeof value === "function" && value(value),
}); });
if (!label && !tooltip) return radioEl; if (!label && !tooltip) return radioEl;
@@ -646,30 +638,32 @@ export const UI = ($, defaultLang = "es") => {
/** RANGE */ /** RANGE */
ui.Range = (props) => { ui.Range = (props) => {
const { label, tooltip, $value, ...rest } = props; const { label, tooltip, value, ...rest } = props;
const rangeEl = $.html("input", { const rangeEl = $.html("input", {
...rest, ...rest,
type: "range", type: "range",
class: joinClass("range", props.$class || props.class), class: joinClass("range", props.class),
$value: $value, value: value,
$disabled: () => val(props.$disabled) || val(props.disabled), disabled: () => val(props.disabled)
oninput: (e) => typeof $value === "function" && $value(e.target.value),
}); });
if (!label && !tooltip) return rangeEl; if (!label && !tooltip) return rangeEl;
const layout = $.html("div", { class: "flex flex-col gap-2" }, [label ? $.html("span", { class: "label-text" }, label) : null, rangeEl]); const layout = $.html("div", { class: "flex flex-col gap-2" }, [
label ? $.html("span", { class: "label-text" }, label) : null,
rangeEl
]);
return tooltip ? $.html("div", { class: "tooltip", "data-tip": tooltip }, layout) : layout; return tooltip ? $.html("div", { class: "tooltip", "data-tip": tooltip }, layout) : layout;
}; };
/** MODAL */ /** MODAL */
ui.Modal = (props, children) => { ui.Modal = (props, children) => {
const { title, buttons, $open, ...rest } = props; const { title, buttons, open, ...rest } = props;
const close = () => $open(false); const close = () => open(false);
return $.if($open, () => return $.if(open, () =>
$.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,
@@ -756,7 +750,7 @@ export const UI = ($, defaultLang = "es") => {
"div", "div",
{ {
...rest, ...rest,
class: () => `dropdown ${val(props.$class) || props.class || ""}`, class: () => `dropdown ${val(props.class) || props.class || ""}`,
}, },
[ [
$.html( $.html(
@@ -782,20 +776,19 @@ export const UI = ($, defaultLang = "es") => {
/** ACCORDION */ /** ACCORDION */
ui.Accordion = (props, children) => { ui.Accordion = (props, children) => {
const { title, name, $open, open, ...rest } = props; const { title, name, open, ...rest } = props;
return $.html( return $.html(
"div", "div",
{ {
...rest, ...rest,
class: joinClass("collapse collapse-arrow bg-base-200 mb-2", props.$class || props.class), class: joinClass("collapse collapse-arrow bg-base-200 mb-2", props.class || props.class),
}, },
[ [
$.html("input", { $.html("input", {
type: name ? "radio" : "checkbox", type: name ? "radio" : "checkbox",
name: name, name: name,
$checked: () => val($open) || val(open), checked: open
onchange: (e) => typeof $open === "function" && $open(e.target.checked),
}), }),
$.html("div", { class: "collapse-title text-xl font-medium" }, title), $.html("div", { class: "collapse-title text-xl font-medium" }, title),
$.html("div", { class: "collapse-content" }, children), $.html("div", { class: "collapse-content" }, children),
@@ -813,7 +806,7 @@ export const UI = ($, defaultLang = "es") => {
"div", "div",
{ {
role: "tablist", role: "tablist",
class: joinClass("tabs tabs-box", props.$class || props.class), class: joinClass("tabs tabs-box", props.class || props.class),
}, },
$.for( $.for(
itemsSignal, itemsSignal,
@@ -841,15 +834,15 @@ export const UI = ($, defaultLang = "es") => {
}; };
/** BADGE */ /** BADGE */
ui.Badge = (props, children) => $.html("span", { ...props, class: joinClass("badge", props.$class || props.class) }, children); ui.Badge = (props, children) => $.html("span", { ...props, class: joinClass("badge", props.class || props.class) }, children);
/** TOOLTIP */ /** TOOLTIP */
ui.Tooltip = (props, children) => ui.Tooltip = (props, children) =>
$.html("div", { ...props, class: joinClass("tooltip", props.$class || props.class), "data-tip": props.tip }, children); $.html("div", { ...props, class: joinClass("tooltip", props.class || props.class), "data-tip": props.tip }, children);
/** NAVBAR */ /** NAVBAR */
ui.Navbar = (props, children) => ui.Navbar = (props, children) =>
$.html("div", { ...props, class: joinClass("navbar bg-base-100 shadow-sm px-4", props.$class || props.class) }, children); $.html("div", { ...props, class: joinClass("navbar bg-base-100 shadow-sm px-4", props.class || props.class) }, children);
/** MENU */ /** MENU */
ui.Menu = (props) => { ui.Menu = (props) => {
@@ -860,32 +853,32 @@ export const UI = ($, defaultLang = "es") => {
$.html("li", {}, [ $.html("li", {}, [
it.children it.children
? $.html("details", { open: it.open }, [ ? $.html("details", { open: it.open }, [
$.html("summary", {}, [it.icon && $.html("span", { class: "mr-2" }, it.icon), it.label]), $.html("summary", {}, [it.icon && $.html("span", { class: "mr-2" }, it.icon), it.label]),
$.html("ul", {}, renderItems(it.children)), $.html("ul", {}, renderItems(it.children)),
]) ])
: $.html("a", { class: () => (val(it.active) ? "active" : ""), onclick: it.onclick }, [ : $.html("a", { class: () => (val(it.active) ? "active" : ""), onclick: it.onclick }, [
it.icon && $.html("span", { class: "mr-2" }, it.icon), it.icon && $.html("span", { class: "mr-2" }, it.icon),
it.label, it.label,
]), ]),
]), ]),
(it, i) => it.label || i, (it, i) => it.label || i,
); );
return $.html("ul", { ...props, class: joinClass("menu bg-base-200 rounded-box", props.$class || props.class) }, renderItems(props.items)); return $.html("ul", { ...props, class: joinClass("menu bg-base-200 rounded-box", props.class || props.class) }, renderItems(props.items));
}; };
/** DRAWER */ /** DRAWER */
ui.Drawer = (props) => ui.Drawer = (props) =>
$.html("div", { class: joinClass("drawer", props.$class || props.class) }, [ $.html("div", { class: joinClass("drawer", props.class || props.class) }, [
$.html("input", { $.html("input", {
id: props.id, id: props.id,
type: "checkbox", type: "checkbox",
class: "drawer-toggle", class: "drawer-toggle",
$checked: props.$open, checked: props.open,
}), }),
$.html("div", { class: "drawer-content" }, props.content), $.html("div", { class: "drawer-content" }, props.content),
$.html("div", { class: "drawer-side" }, [ $.html("div", { class: "drawer-side" }, [
$.html("label", { for: props.id, class: "drawer-overlay", onclick: () => props.$open?.(false) }), $.html("label", { for: props.id, class: "drawer-overlay", onclick: () => props.open?.(false) }),
$.html("div", { class: "min-h-full bg-base-200 w-80" }, props.side), $.html("div", { class: "min-h-full bg-base-200 w-80" }, props.side),
]), ]),
]); ]);
@@ -896,7 +889,7 @@ export const UI = ($, defaultLang = "es") => {
"fieldset", "fieldset",
{ {
...props, ...props,
class: joinClass("fieldset bg-base-200 border border-base-300 p-4 rounded-lg", props.$class || props.class), class: joinClass("fieldset bg-base-200 border border-base-300 p-4 rounded-lg", props.class || props.class),
}, },
[ [
() => { () => {
@@ -924,24 +917,23 @@ export const UI = ($, defaultLang = "es") => {
}; };
/** STACK */ /** STACK */
ui.Stack = (props, children) => $.html("div", { ...props, class: joinClass("stack", props.$class || props.class) }, children); ui.Stack = (props, children) => $.html("div", { ...props, class: joinClass("stack", props.class || props.class) }, children);
/** STAT */ /** STAT */
ui.Stat = (props) => ui.Stat = (props) =>
$.html("div", { ...props, class: joinClass("stat", props.$class || props.class) }, [ $.html("div", { ...props, class: joinClass("stat", props.class || props.class) }, [
props.icon && $.html("div", { class: "stat-figure text-secondary" }, props.icon), props.icon && $.html("div", { class: "stat-figure text-secondary" }, props.icon),
props.label && $.html("div", { class: "stat-title" }, props.label), props.label && $.html("div", { class: "stat-title" }, props.label),
$.html("div", { class: "stat-value" }, () => val(props.$value) ?? props.value), $.html("div", { class: "stat-value" }, () => val(props.value) ?? props.value),
props.desc && $.html("div", { class: "stat-desc" }, props.desc), props.desc && $.html("div", { class: "stat-desc" }, props.desc),
]); ]);
/** SWAP */ /** SWAP */
ui.Swap = (props) => ui.Swap = (props) =>
$.html("label", { class: joinClass("swap", props.$class || props.class) }, [ $.html("label", { class: joinClass("swap", props.class || props.class) }, [
$.html("input", { $.html("input", {
type: "checkbox", type: "checkbox",
$checked: props.$value, checked: props.value
onchange: (e) => props.$value?.(e.target.checked),
}), }),
$.html("div", { class: "swap-on" }, props.on), $.html("div", { class: "swap-on" }, props.on),
$.html("div", { class: "swap-off" }, props.off), $.html("div", { class: "swap-off" }, props.off),
@@ -949,37 +941,36 @@ export const UI = ($, defaultLang = "es") => {
/** INDICATOR */ /** INDICATOR */
ui.Indicator = (props, children) => ui.Indicator = (props, children) =>
$.html("div", { class: joinClass("indicator", props.$class || props.class) }, [ $.html("div", { class: joinClass("indicator", props.class || props.class) }, [
children, children,
$.html("span", { class: joinClass("indicator-item badge", props.badgeClass) }, props.badge), $.html("span", { class: joinClass("indicator-item badge", props.badgeClass) }, props.badge),
]); ]);
/** RATING */ /** RATING */
ui.Rating = (props) => { ui.Rating = (props) => {
const { $value, count = 5, name = `rating-${Math.random().toString(36).slice(2, 7)}`, mask = "mask-star", readonly = false, ...rest } = props; const { value, count = 5, mask = "mask-star", readonly = false, ...rest } = props;
const ratingGroup = `rating-${Math.random().toString(36).slice(2, 7)}`;
return $.html( return $.html("div", {
"div", ...rest,
{ class: () => `rating ${val(readonly) ? "pointer-events-none" : ""} ${props.class || ""}`,
...rest, },
class: () => `rating ${val(readonly) ? "pointer-events-none" : ""} ${props.class || ""}`,
},
Array.from({ length: val(count) }, (_, i) => { Array.from({ length: val(count) }, (_, i) => {
const ratingValue = i + 1; const starValue = i + 1;
return $.html("input", { return $.html("input", {
type: "radio", type: "radio",
name: name, name: ratingGroup,
class: `mask ${mask}`, class: `mask ${mask}`,
"aria-label": `${ratingValue} star`, "aria-label": `${starValue} star`,
checked: () => Math.round(val($value)) === ratingValue, checked: () => Math.round(val(value)) === starValue,
onchange: () => { onchange: () => {
if (!val(readonly) && typeof $value === "function") { if (!val(readonly) && typeof value === "function") {
$value(ratingValue); value(starValue);
} }
}, },
}); });
}), }));
);
}; };
/** ALERT */ /** ALERT */
@@ -1180,8 +1171,13 @@ export const UI = ($, defaultLang = "es") => {
ui.tt = tt; ui.tt = tt;
Object.keys(ui).forEach((key) => { Object.keys(ui).forEach((key) => {
window[key] = ui[key];
$[key] = ui[key]; $[key] = ui[key];
Object.defineProperty(window, key, {
value: ui[key],
writable: false,
configurable: true,
enumerable: true
});
}); });
return ui; return ui;

2
UI/sigpro-ui.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -313,7 +313,7 @@
return outlet; return outlet;
}; };
$.go = (p) => (window.location.hash = p.replace(/^#?\/?/, "#/")); $.go = (path) => (window.location.hash = path.replace(/^#?\/?/, "#/"));
$.mount = (component, target) => { $.mount = (component, target) => {
const el = typeof target === "string" ? document.querySelector(target) : target; const el = typeof target === "string" ? document.querySelector(target) : target;
@@ -326,8 +326,15 @@
}; };
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(/\s+/); 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(/\s+/);
tags.forEach((t) => { tags.forEach((tagName) => {
window[t.charAt(0).toUpperCase() + t.slice(1)] = (p, c) => $.html(t, p, c); const helperName = tagName.charAt(0).toUpperCase() + tagName.slice(1);
Object.defineProperty(window, helperName, {
value: (props, content) => $.html(tagName, props, content),
writable: false,
configurable: true,
enumerable: true
});
}); });
window.$ = $; window.$ = $;

View File

@@ -21,40 +21,48 @@ $.html(tagName: string, props?: Object, children?: any[] | any): HTMLElement
### 1. Attribute Handling ### 1. Attribute Handling
SigPro intelligently decides how to apply each property: SigPro intelligently decides how to apply each property:
* **Standard Props**: Applied via `setAttribute` or direct property assignment. * **Standard Props**: Applied via `setAttribute` or direct property assignment.
* **Boolean Props**: Uses `toggleAttribute` (e.g., `checked`, `disabled`, `hidden`).
* **Class Names**: Supports `class` or `className` interchangeably. * **Class Names**: Supports `class` or `className` interchangeably.
* **Boolean Props**: Automatic handling for `checked`, `disabled`, `hidden`, etc.
### 2. Event Listeners & Modifiers ### 2. Event Listeners
Events are defined by the `on` prefix. SigPro supports **Dot Notation** for common event operations: Events are defined by the `on` prefix. SigPro automatically registers the listener and ensures it is cleaned up when the element is destroyed.
```javascript ```javascript
$.html("button", { Button({
// e.preventDefault() is called automatically onclick: (e) => console.log("Clicked!", e),
"onsubmit.prevent": (e) => save(e),
// e.stopPropagation() is called automatically
"onclick.stop": () => console.log("No bubbling"),
// { once: true } listener option
"onclick.once": () => console.log("Runs only once")
}, "Click Me"); }, "Click Me");
``` ```
### 3. Reactive Attributes ### 3. Reactive Attributes (One-Way)
If an attribute value is a **function** (like a Signal), `$.html` creates an internal **`$.watch`** to keep the DOM in sync with the state. If an attribute value is a **function** (like a Signal), `$.html` creates an internal **`$.watch`** to keep the DOM in sync with the state.
```javascript ```javascript
$.html("div", { Div({
// Updates the class whenever 'theme()' changes // Updates the class whenever 'theme()' changes
class: () => theme() === "dark" ? "bg-black" : "bg-white" class: () => theme() === "dark" ? "bg-black" : "bg-white"
}); });
``` ```
### 4. Reactive Children ### 4. Smart Two-Way Binding (Automatic)
SigPro automatically enables **bidirectional synchronization** when it detects a **Signal** assigned to a form-capable attribute (`value` or `checked`) on an input element (`input`, `textarea`, `select`).
```javascript
// Syncs input value <-> signal automatically
Input({
type: "text",
value: username // No special symbols needed!
})
```
> **Note:** To use a Signal as **read-only** in an input, wrap it in an anonymous function: `value: () => username()`.
### 5. Reactive Children
Children can be static or dynamic. When a child is a function, SigPro creates a reactive boundary using `$.watch` for that specific part of the DOM. Children can be static or dynamic. When a child is a function, SigPro creates a reactive boundary using `$.watch` for that specific part of the DOM.
```javascript ```javascript
$.html("div", {}, [ Div({}, [
H1("Static Title"), H1("Static Title"),
// Only this text node re-renders when 'count' changes // Only this text node re-renders when 'count' changes
() => `Current count: ${count()}` () => `Current count: ${count()}`
@@ -63,27 +71,16 @@ $.html("div", {}, [
--- ---
## 🔄 Two-Way Binding Operator (`$`)
When a property starts with `$`, `$.html` enables bidirectional synchronization. This is primarily used for form inputs.
```javascript
$.html("input", {
type: "text",
$value: username // Syncs input value <-> signal
});
```
## 🧹 Memory Management (Internal) ## 🧹 Memory Management (Internal)
Every element created with `$.html` is "self-aware" regarding its reactive dependencies. Every element created with `$.html` is "self-aware" regarding its reactive dependencies.
* **`._cleanups`**: A hidden `Set` attached to the element that stores all `stop()` functions from its internal `$.watch` calls and event listeners. * **`._cleanups`**: A hidden `Set` attached to the element that stores all `stop()` functions from its internal `$.watch` calls and event listeners.
* **Lifecycle**: When an element is removed by a Controller (`$.If`, `$.For`, or `$.router`), SigPro performs a recursive "sweep" to execute these cleanups, ensuring **zero memory leaks**. * **Lifecycle**: When an element is removed by a Controller (`$.if`, `$.for`, or `$.router`), SigPro performs a recursive **"sweep"** to execute these cleanups, ensuring **zero memory leaks**.
--- ---
## 💡 Tag Constructors (The Shortcuts) ## 💡 Tag Constructors (The Shortcuts)
Instead of writing `$.html("div", ...)` every time, SigPro provides PascalCase global functions: Instead of writing `$.html("div", ...)` every time, SigPro provides PascalCase global functions for all standard HTML tags:
```javascript ```javascript
// This: // This:

View File

@@ -13,7 +13,7 @@ SigPro iterates through a manifest of standard HTML tags and attaches a wrapper
## 2. The Complete Global Registry ## 2. The Complete Global Registry
The following functions are injected into the global scope (using **PascalCase** to prevent naming collisions with common JS variables) and are ready to use: The following functions are injected into the global scope using **PascalCase** to prevent naming collisions with common JS variables:
| Category | Available Global Functions | | Category | Available Global Functions |
| :--- | :--- | | :--- | :--- |
@@ -24,8 +24,6 @@ The following functions are injected into the global scope (using **PascalCase**
| **Tables** | `Table`, `Thead`, `Tbody`, `Tr`, `Th`, `Td`, `Tfoot`, `Caption` | | **Tables** | `Table`, `Thead`, `Tbody`, `Tr`, `Th`, `Td`, `Tfoot`, `Caption` |
| **Media** | `Img`, `Canvas`, `Video`, `Audio`, `Svg`, `Iframe`, `Picture`, `Source` | | **Media** | `Img`, `Canvas`, `Video`, `Audio`, `Svg`, `Iframe`, `Picture`, `Source` |
> **The SigPro Philosophy:** Tags are not "magic strings" handled by a compiler. They are **functional constructors**. Every time you call `Div()`, you execute a pure JS function that returns a real, reactive DOM element.
--- ---
## 3. Usage Patterns (Smart Arguments) ## 3. Usage Patterns (Smart Arguments)
@@ -33,7 +31,6 @@ The following functions are injected into the global scope (using **PascalCase**
SigPro tag helpers are flexible. They automatically detect if you are passing attributes, children, or both. SigPro tag helpers are flexible. They automatically detect if you are passing attributes, children, or both.
### A. Attributes + Children ### A. Attributes + Children
The standard way to build structured UI.
```javascript ```javascript
Div({ class: 'container', id: 'main' }, [ Div({ class: 'container', id: 'main' }, [
H1("Welcome to SigPro"), H1("Welcome to SigPro"),
@@ -42,7 +39,7 @@ Div({ class: 'container', id: 'main' }, [
``` ```
### B. Children Only (The "Skipper") ### B. Children Only (The "Skipper")
If you don't need attributes, you can skip the object and pass the content (string, array, or function) directly as the first argument. If you don't need attributes, you can pass the content directly as the first argument.
```javascript ```javascript
Section([ Section([
H2("Clean Syntax"), H2("Clean Syntax"),
@@ -50,85 +47,79 @@ Section([
]); ]);
``` ```
### C. Primitive Content
For simple tags, just pass a string or a number.
```javascript
H1("Hello World");
Span(42);
```
--- ---
## 4. Reactive Power ## 4. Reactive Power
These helpers are natively wired into SigPro's **`$.watch`** engine. No manual effect management is needed; the lifecycle is tied to the DOM node. These helpers are natively wired into SigPro's **`$.watch`** engine.
### Reactive Attributes ### Reactive Attributes (One-Way)
Simply pass a Signal (function) to any attribute. SigPro creates an internal `$.watch` to keep the DOM in sync. Simply pass a Signal (function) to any attribute. SigPro creates an internal `$.watch` to keep the DOM in sync.
```javascript ```javascript
const theme = $("light"); const theme = $("light");
Div({ Div({
// Updates 'class' automatically via internal $.watch
class: () => `app-box ${theme()}` class: () => `app-box ${theme()}`
}, "Themeable Box"); }, "Themeable Box");
``` ```
### The Binding Operator (`$`) ### Smart Two-Way Binding (Automatic)
Use the `$` prefix for **Two-Way Binding** on inputs. This bridges the Signal and the Input element bi-directionally. SigPro automatically bridges the **Signal** and the **Input** element bi-directionally when you assign a Signal to `value` or `checked`. No special operators are required.
```javascript ```javascript
const search = $(""); const search = $("");
// UI updates Signal AND Signal updates UI automatically
Input({ Input({
type: "text", type: "text",
placeholder: "Search...", placeholder: "Search...",
$value: search // UI updates Signal AND Signal updates UI value: search
}); });
``` ```
> **Pro Tip:** If you want an input to be **read-only** but still reactive, wrap the signal in an anonymous function: `value: () => search()`. This prevents the "backwards" synchronization.
### Dynamic Flow & Cleanup ### Dynamic Flow & Cleanup
Combine tags with Core controllers for high-performance rendering. SigPro automatically cleans up the `$.watch` instances when nodes are removed. Combine tags with Core controllers. SigPro automatically cleans up the `$.watch` instances and event listeners when nodes are removed from the DOM.
```javascript ```javascript
const items = $(["Apple", "Banana", "Cherry"]); const items = $(["Apple", "Banana", "Cherry"]);
Ul({ class: "list-disc" }, [ Ul({ class: "list-disc" }, [
$.For(items, (item) => Li(item)) $.for(items, (item) => Li(item), (item) => item)
]); ]);
``` ```
--- ---
::: danger ::: danger
## ⚠️ Important: Naming Conventions ## ⚠️ Important: Naming Conventions
Since SigPro injects these helpers into the global `window` object, follow these rules to avoid bugs: 1. **Avoid Shadowing**: Don't name your local variables like the tags (e.g., `const Div = ...`). This will "hide" the global SigPro helper.
2. **Custom Components**: Always use **PascalCase** for your own component functions (e.g., `UserCard`, `NavMenu`) to distinguish them from built-in Tag Helpers.
1. **Avoid Shadowing**: Don't name your local variables like the tags (e.g., `const Div = ...`). This will "hide" the SigPro helper.
2. **Custom Components**: Always use **PascalCase** for your own component functions (e.g., `UserCard`, `NavMenu`) to distinguish them from the built-in Tag Helpers and maintain architectural clarity.
::: :::
--- ---
## 5. Logic to UI Comparison ## 5. Logic to UI Comparison
Here is how a dynamic **User Status** component translates from SigPro logic to the final DOM structure, handled by the engine. Here is how a dynamic **User Status** component translates from SigPro logic to the final DOM structure.
```javascript ```javascript
// SigPro Component const UserStatus = (name, online) => (
const UserStatus = (name, $online) => (
Div({ class: 'flex items-center gap-2' }, [ Div({ class: 'flex items-center gap-2' }, [
Span({ Span({
// Boolean toggle for 'hidden' attribute hidden: () => !online(),
hidden: () => !$online(),
class: 'w-3 h-3 bg-green-500 rounded-full' class: 'w-3 h-3 bg-green-500 rounded-full'
}), }),
P({ P({
// Reactive text content via automatic $.watch class: () => online() ? "text-bold" : "text-gray-400"
class: () => $online() ? "text-bold" : "text-gray-400"
}, name) }, name)
]) ])
); );
``` ```
| State (`$online`) | Rendered HTML | Memory Management | | State (`online`) | Rendered HTML | Memory Management |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **`true`** | `<div class="flex..."><span class="w-3..."></span><p class="text-bold">John</p></div>` | Watcher active | | **`true`** | `<div class="flex..."><span class="w-3..."></span><p class="text-bold">John</p></div>` | Watcher active |
| **`false`** | `<div class="flex..."><span hidden class="w-3..."></span><p class="text-gray-400">John</p></div>` | Attribute synced | | **`false`** | `<div class="flex..."><span hidden class="w-3..."></span><p class="text-gray-400">John</p></div>` | Attribute synced |

51
src/docs/ui/buttons.md Normal file
View File

@@ -0,0 +1,51 @@
# Buttons
The **SigPro** Button component wraps [DaisyUI 5](https://daisyui.com/components/button/) styles with native reactive logic.
## Basic Usage
<button class="btn btn-primary btn-secondary btn-accent btn-neutral btn-info btn-lg btn-sm btn-xs">Test</button>
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
if (typeof window === 'undefined') return;
const init = () => {
// 1. Esperamos a que el Core ($) y los helpers (Button, Div) existan
if (!window.$ || !window.Button || !window.Input) {
setTimeout(init, 100);
return;
}
// 2. Usamos las funciones tal cual las crea tu Core (con Mayúscula inicial)
const { $, Button, Input, Div, P, Span } = window;
const Mount = $.mount;
const HTML = $.html;
// --- DEMO REACTIVA ---
const nombre = $('Mundo');
return Mount(
Div({ class: 'flex flex-col gap-4' }, [
// Usamos el helper Input de tu librería
Input({
class: 'input input-bordered input-primary w-full max-w-xs',
value: nombre, // Tu Core maneja el binding si es una señal
placeholder: 'Escribe tu nombre...'
}),
// El P y el Span también son helpers de tu Core
P({ class: 'text-xl' }, [
'Hola ',
Span({ class: 'text-primary font-bold' }, nombre),
'!'
])
]),
'#demo-input-simple'
);
};
init();
})
</script>