Next Preview Work Final

This commit is contained in:
2026-04-03 23:54:11 +02:00
parent 257107e198
commit a6705621d8
49 changed files with 1119 additions and 493 deletions

226
dist/sigpro-ui.js vendored
View File

@@ -68,7 +68,7 @@
Accordion: () => Accordion Accordion: () => Accordion
}); });
// node_modules/sigpro/sigpro/index.js // src/sigpro.js
var activeEffect = null; var activeEffect = null;
var currentOwner = null; var currentOwner = null;
var effectQueue = new Set; var effectQueue = new Set;
@@ -272,6 +272,7 @@
} }
const el = document.createElement(tag), _sanitize = (key, val) => (key === "src" || key === "href") && String(val).toLowerCase().includes("javascript:") ? "#" : val; const el = document.createElement(tag), _sanitize = (key, val) => (key === "src" || key === "href") && String(val).toLowerCase().includes("javascript:") ? "#" : val;
el._cleanups = new Set; el._cleanups = new Set;
const boolAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
for (let [key, val] of Object.entries(props)) { for (let [key, val] of Object.entries(props)) {
if (key === "ref") { if (key === "ref") {
typeof val === "function" ? val(el) : val.current = el; typeof val === "function" ? val(el) : val.current = el;
@@ -294,33 +295,54 @@
} else if (isSignal) { } else if (isSignal) {
el._cleanups.add($watch2(() => { el._cleanups.add($watch2(() => {
const currentVal = _sanitize(key, val()); const currentVal = _sanitize(key, val());
if (key === "class") if (key === "class") {
el.className = currentVal || ""; el.className = currentVal || "";
else } else if (boolAttrs.includes(key)) {
if (currentVal) {
el.setAttribute(key, "");
el[key] = true;
} else {
el.removeAttribute(key);
el[key] = false;
}
} else {
currentVal == null ? el.removeAttribute(key) : el.setAttribute(key, currentVal); currentVal == null ? el.removeAttribute(key) : el.setAttribute(key, currentVal);
}
})); }));
} else { } else {
el.setAttribute(key, _sanitize(key, val)); if (boolAttrs.includes(key)) {
if (val) {
el.setAttribute(key, "");
el[key] = true;
} else {
el.removeAttribute(key);
el[key] = false;
}
} else {
el.setAttribute(key, _sanitize(key, val));
}
} }
} }
const append = (child) => { const append = (child) => {
if (Array.isArray(child)) if (Array.isArray(child))
return child.forEach(append); return child.forEach(append);
if (typeof child === "function") { if (child instanceof Node) {
el.appendChild(child);
} else if (typeof child === "function") {
const marker = document.createTextNode(""); const marker = document.createTextNode("");
el.appendChild(marker); el.appendChild(marker);
let nodes = []; let nodes = [];
el._cleanups.add($watch2(() => { el._cleanups.add($watch2(() => {
const result = child(), nextNodes = (Array.isArray(result) ? result : [result]).map((item) => item?._isRuntime ? item.container : item instanceof Node ? item : document.createTextNode(item ?? "")); const res = child(), next = (Array.isArray(res) ? res : [res]).map((i) => i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? ""));
nodes.forEach((node) => { nodes.forEach((n) => {
sweep(node); sweep?.(n);
node.remove(); n.remove();
}); });
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker)); next.forEach((n) => marker.parentNode?.insertBefore(n, marker));
nodes = nextNodes; nodes = next;
})); }));
} else } else
el.appendChild(child instanceof Node ? child : document.createTextNode(child ?? "")); el.appendChild(document.createTextNode(child ?? ""));
}; };
append(content); append(content);
return el; return el;
@@ -345,9 +367,9 @@
return container; return container;
}; };
$if.not = (condition, thenVal, otherwiseVal) => $if(() => !(typeof condition === "function" ? condition() : condition), thenVal, otherwiseVal); $if.not = (condition, thenVal, otherwiseVal) => $if(() => !(typeof condition === "function" ? condition() : condition), thenVal, otherwiseVal);
var $for = (source, render, keyFn) => { var $for = (source, render, keyFn, tag = "div", props = { style: "display:contents" }) => {
const marker = document.createTextNode(""); const marker = document.createTextNode("");
const container = $html2("div", { style: "display:contents" }, [marker]); const container = $html2(tag, props, [marker]);
let cache = new Map; let cache = new Map;
$watch2(() => { $watch2(() => {
const items = (typeof source === "function" ? source() : source) || []; const items = (typeof source === "function" ? source() : source) || [];
@@ -450,6 +472,7 @@
}; };
install(SigProCore); install(SigProCore);
} }
// src/components/index.js // src/components/index.js
var exports_components = {}; var exports_components = {};
__export(exports_components, { __export(exports_components, {
@@ -617,10 +640,12 @@
placeholder, placeholder,
disabled, disabled,
size, size,
validate,
...rest ...rest
} = props; } = props;
const isPassword = type === "password"; const isPassword = type === "password";
const visible = $(false); const visible = $(false);
const errorMsg = $(null);
const iconMap = { const iconMap = {
text: "icon-[lucide--text]", text: "icon-[lucide--text]",
password: "icon-[lucide--lock]", password: "icon-[lucide--lock]",
@@ -644,16 +669,35 @@
return "btn-lg"; return "btn-lg";
return "btn-md"; return "btn-md";
}; };
const handleInput = (e) => {
const newValue = e.target.value;
if (validate) {
const result = validate(newValue);
errorMsg(result || null);
}
oninput?.(e);
};
const hasError = () => errorMsg() && errorMsg() !== "";
const inputClasses = () => {
let classes = `input w-full ${paddingLeft} ${paddingRight}`;
if (className)
classes += ` ${className}`;
if (hasError())
classes += " input-error";
return classes.trim();
};
const inputElement = $html2("input", {
...rest,
type: () => isPassword ? visible() ? "text" : "password" : type,
placeholder: placeholder || " ",
class: inputClasses,
value,
oninput: handleInput,
disabled: () => val(disabled),
"aria-invalid": () => hasError() ? "true" : "false"
});
return $html2("div", { class: "relative w-full" }, () => [ return $html2("div", { class: "relative w-full" }, () => [
$html2("input", { inputElement,
...rest,
type: () => isPassword ? visible() ? "text" : "password" : type,
placeholder: placeholder || " ",
class: ui("input w-full", `${paddingLeft} ${paddingRight} ${className || ""}`.trim()),
value,
oninput: (e) => oninput?.(e),
disabled: () => val(disabled)
}),
leftIcon ? $html2("div", { leftIcon ? $html2("div", {
class: "absolute left-3 inset-y-0 flex items-center pointer-events-none text-base-content/60" class: "absolute left-3 inset-y-0 flex items-center pointer-events-none text-base-content/60"
}, leftIcon) : null, }, leftIcon) : null,
@@ -664,7 +708,10 @@
e.preventDefault(); e.preventDefault();
visible(!visible()); visible(!visible());
} }
}, () => getPasswordIcon()) : null }, () => getPasswordIcon()) : null,
$html2("div", {
class: "text-error text-xs mt-1 px-3 absolute -bottom-5 left-0"
}, () => hasError() ? errorMsg() : null)
]); ]);
}; };
@@ -1353,7 +1400,7 @@
List: () => List List: () => List
}); });
var List = (props) => { var List = (props) => {
const { class: className, items, header, render, keyFn = (item, index) => index, ...rest } = props; const { class: className, items, header, render, keyFn = (item, index) => item.id ?? index, ...rest } = props;
const listItems = $for(items, (item, index) => $html2("li", { class: "list-row" }, [render(item, index)]), keyFn); const listItems = $for(items, (item, index) => $html2("li", { class: "list-row" }, [render(item, index)]), keyFn);
return $html2("ul", { return $html2("ul", {
...rest, ...rest,
@@ -1607,6 +1654,7 @@
const pinRowsClass = val(pinRows) ? "table-pin-rows" : ""; const pinRowsClass = val(pinRows) ? "table-pin-rows" : "";
return ui("table", className, zebraClass, pinRowsClass); return ui("table", className, zebraClass, pinRowsClass);
}; };
const getInternalKeyFn = keyFn || ((item, idx) => item.id || idx);
return $html2("div", { class: "overflow-x-auto w-full bg-base-100 rounded-box border border-base-300" }, [ return $html2("div", { class: "overflow-x-auto w-full bg-base-100 rounded-box border border-base-300" }, [
$html2("table", { ...rest, class: tableClass }, [ $html2("table", { ...rest, class: tableClass }, [
$html2("thead", {}, [ $html2("thead", {}, [
@@ -1614,25 +1662,27 @@
]), ]),
$html2("tbody", {}, [ $html2("tbody", {}, [
$for(items, (item, index) => { $for(items, (item, index) => {
const it = () => {
const currentItems = val(items);
const key = getInternalKeyFn(item, index);
return currentItems.find((u, i) => getInternalKeyFn(u, i) === key) || item;
};
return $html2("tr", { class: "hover" }, columns.map((col) => { return $html2("tr", { class: "hover" }, columns.map((col) => {
const cellContent = () => { const cellContent = () => {
const latestItem = it();
if (col.render) if (col.render)
return col.render(item, index); return col.render(latestItem, index);
const value = item[col.key]; return val(latestItem[col.key]);
return val(value);
}; };
return $html2("td", { class: col.class || "" }, [cellContent]); return $html2("td", { class: col.class || "" }, [cellContent]);
})); }));
}, keyFn || ((item, idx) => item.id || idx)), }, getInternalKeyFn),
$if(() => val(items).length === 0, () => $html2("tr", {}, [ $if(() => val(items).length === 0, () => $html2("tr", {}, [
$html2("td", { colspan: columns.length, class: "text-center p-10 opacity-50" }, [ $html2("td", { colspan: columns.length, class: "text-center p-10 opacity-50" }, [
val(empty) val(empty)
]) ])
])) ]))
]), ])
$if(() => columns.some((c) => c.footer), () => $html2("tfoot", {}, [
$html2("tr", {}, columns.map((col) => $html2("th", {}, col.footer || "")))
]))
]) ])
]); ]);
}; };
@@ -1645,55 +1695,55 @@
var Tabs = (props) => { var Tabs = (props) => {
const { items, class: className, ...rest } = props; const { items, class: className, ...rest } = props;
const itemsSignal = typeof items === "function" ? items : () => items || []; const itemsSignal = typeof items === "function" ? items : () => items || [];
const name = `tabs-${Math.random().toString(36).slice(2, 9)}`; const activeIndex = $(0);
const getActiveIndex = () => { $watch(() => {
const arr = itemsSignal(); const idx = itemsSignal().findIndex((it) => val(it.active) === true);
const idx = arr.findIndex((it) => val(it.active) === true); if (idx !== -1 && idx !== activeIndex())
return idx === -1 ? 0 : idx; activeIndex(idx);
}; });
const activeIndex = $(getActiveIndex); return $html2("div", { ...rest, class: "w-full" }, [
const updateActiveIndex = () => { $html2("div", {
const newIndex = getActiveIndex(); role: "tablist",
if (newIndex !== activeIndex()) class: ui("tabs", className || "tabs-box")
activeIndex(newIndex); }, () => {
}; const list = itemsSignal();
$watch(() => updateActiveIndex()); return list.map((it, idx) => {
return $html2("div", { const isSelected = () => activeIndex() === idx;
...rest, const tab = $html2("button", {
class: ui("tabs", className || "tabs-box") role: "tab",
}, [ class: () => ui("tab", isSelected() ? "tab-active" : ""),
$for(itemsSignal, (it, idx) => { onclick: (e) => {
const isChecked = () => activeIndex() === idx; e.preventDefault();
const getLabelText = () => { if (!val(it.disabled)) {
const label = typeof it.label === "function" ? it.label() : it.label;
return typeof label === "string" ? label : `Tab ${idx + 1}`;
};
return [
$html2("input", {
type: "radio",
name,
class: "tab",
"aria-label": getLabelText(),
checked: isChecked,
disabled: () => val(it.disabled),
onchange: (e) => {
if (e.target.checked && !val(it.disabled)) {
if (it.onclick) if (it.onclick)
it.onclick(); it.onclick();
if (typeof it.active === "function")
it.active(true);
activeIndex(idx); activeIndex(idx);
} }
} }
}), });
$html2("div", { $watch(() => {
const content = val(it.label);
if (content instanceof Node) {
tab.replaceChildren(content);
} else {
tab.textContent = String(content);
}
});
return tab;
});
}),
$html2("div", { class: "tab-panels" }, () => {
return itemsSignal().map((it, idx) => {
const isVisible = () => activeIndex() === idx;
return $html2("div", {
role: "tabpanel",
class: "tab-content bg-base-100 border-base-300 p-6", class: "tab-content bg-base-100 border-base-300 p-6",
style: () => isChecked() ? "display: block" : "display: none" style: () => isVisible() ? "display: block" : "display: none"
}, [ }, [
typeof it.content === "function" ? it.content() : it.content () => typeof it.content === "function" ? it.content() : it.content
]) ]);
]; });
}, (it, idx) => idx) })
]); ]);
}; };
@@ -1710,27 +1760,29 @@
warning: "icon-[lucide--alert-triangle]", warning: "icon-[lucide--alert-triangle]",
error: "icon-[lucide--alert-circle]" error: "icon-[lucide--alert-circle]"
}; };
const itemsSource = typeof items === "function" ? items : () => items || [];
return $html2("ul", { return $html2("ul", {
...rest, ...rest,
class: () => ui(`timeline ${val(vertical) ? "timeline-vertical" : "timeline-horizontal"} ${val(compact) ? "timeline-compact" : ""}`, className) class: () => ui(`timeline ${val(vertical) ? "timeline-vertical" : "timeline-horizontal"} ${val(compact) ? "timeline-compact" : ""}`, className)
}, [ }, () => {
$for(itemsSource, (item, i) => { const list = (typeof items === "function" ? items() : items) || [];
return list.map((item, i) => {
const isFirst = i === 0; const isFirst = i === 0;
const isLast = i === itemsSource().length - 1; const isLast = i === list.length - 1;
const itemType = item.type || "success"; const itemType = item.type || "success";
const isCompleted = () => val(item.completed);
const prevCompleted = () => i > 0 && val(list[i - 1].completed);
const renderSlot = (content) => typeof content === "function" ? content() : content; const renderSlot = (content) => typeof content === "function" ? content() : content;
return $html2("li", { class: "flex-1" }, [ return $html2("li", { class: "flex-1" }, [
!isFirst ? $html2("hr", { class: () => item.completed ? "bg-primary" : "" }) : null, !isFirst ? $html2("hr", { class: () => prevCompleted() ? "bg-primary" : "" }) : null,
$html2("div", { class: "timeline-start" }, () => renderSlot(item.title)), $html2("div", { class: "timeline-start" }, [() => renderSlot(item.title)]),
$html2("div", { class: "timeline-middle" }, () => [ $html2("div", { class: "timeline-middle" }, [
item.icon ? getIcon(item.icon) : getIcon(iconMap[itemType] || iconMap.success) () => item.icon ? getIcon(item.icon) : getIcon(iconMap[itemType] || iconMap.success)
]), ]),
$html2("div", { class: "timeline-end timeline-box shadow-sm" }, () => renderSlot(item.detail)), $html2("div", { class: "timeline-end timeline-box shadow-sm" }, [() => renderSlot(item.detail)]),
!isLast ? $html2("hr", { class: () => item.completed ? "bg-primary" : "" }) : null !isLast ? $html2("hr", { class: () => isCompleted() ? "bg-primary" : "" }) : null
]); ]);
}, (item, i) => item.id || i) });
]); });
}; };
// src/components/Toast.js // src/components/Toast.js

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@
</div> </div>
<h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-secondary via-accent to-primary !text-center w-full">SigPro UI beta (W.I.P.)</h1> <h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-secondary via-accent to-primary !text-center w-full">SigPro UI beta (W.I.P.)</h1>
<div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Reactive Design System for SigPro</div> <div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Reactive Design System for SigPro</div>
<div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"Atomic components for high-performance interfaces. Zero-boilerplate, pure DaisyUI v5 elegance."</div> <div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"Atomic components for high-performance interfaces. Zero-boilerplate, pure reactivity."</div>
<div class="flex flex-wrap justify-center gap-4 w-full"> <div class="flex flex-wrap justify-center gap-4 w-full">
<a href="#/install" class="btn btn-secondary btn-lg shadow-xl shadow-secondary/20 group px-10 border-none text-secondary-content">View Components <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a> <a href="#/install" class="btn btn-secondary btn-lg shadow-xl shadow-secondary/20 group px-10 border-none text-secondary-content">View Components <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a>
<button onclick="window.open('https://github.com/natxocc/sigpro-ui')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">GitHub</button> <button onclick="window.open('https://github.com/natxocc/sigpro-ui')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">GitHub</button>
@@ -25,26 +25,26 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch">
<div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"> <div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1">
<div class="card-body p-6"> <div class="card-body p-6">
<h3 class="card-title text-xl font-black text-secondary italic">TAILWIND V4</h3> <h3 class="card-title text-xl font-black text-secondary italic">SIGPRO NATIVE</h3>
<p class="text-sm opacity-70">Built on the latest CSS engine. Lightning fast styles with zero legacy overhead.</p> <p class="text-sm opacity-70">Direct integration with SigPro signals. No wrappers, no context, just pure atomic reactivity.</p>
</div> </div>
</div> </div>
<div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"> <div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1">
<div class="card-body p-6"> <div class="card-body p-6">
<h3 class="card-title text-xl font-black text-accent italic">DAISYUI V5</h3> <h3 class="card-title text-xl font-black text-accent italic">ZERO CONFIG</h3>
<p class="text-sm opacity-70">Semantic, beautiful and accessible. Professional components out of the box.</p> <p class="text-sm opacity-70">Import and build immediately. Designed for developers who hate configuration files.</p>
</div> </div>
</div> </div>
<div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"> <div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1">
<div class="card-body p-6"> <div class="card-body p-6">
<h3 class="card-title text-xl font-black text-primary italic">NATIVE REACTION</h3> <h3 class="card-title text-xl font-black text-primary italic">REACTIVE BY DESIGN</h3>
<p class="text-sm opacity-70">Direct integration with SigPro signals. No wrappers, no context, just speed.</p> <p class="text-sm opacity-70">Every component is a high-order function optimized for SigPro's fine-grained updates.</p>
</div> </div>
</div> </div>
<div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"> <div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1">
<div class="card-body p-6"> <div class="card-body p-6">
<h3 class="card-title text-xl font-black italic text-base-content">READY-TO-GO</h3> <h3 class="card-title text-xl font-black italic text-base-content">READY-TO-GO</h3>
<p class="text-sm opacity-70">Import and build. Designed for developers who hate configuration files.</p> <p class="text-sm opacity-70">60+ atomic components. Semantic, accessible, and production-ready.</p>
</div> </div>
</div> </div>
</div> </div>
@@ -54,13 +54,13 @@
SigPro-UI isn't just a library; it's a **Functional Design System**. SigPro-UI isn't just a library; it's a **Functional Design System**.
It eliminates the gap between your data (Signals) and your layout (DaisyUI). Each component is a high-order function optimized for the SigPro core, ensuring that your UI only updates where it matters. It eliminates the gap between your data (Signals) and your UI components. Each component is a high-order function optimized for the SigPro core, ensuring that your interface only updates where it matters.
| Requirement | Value | Why? | | Requirement | Value | Why? |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Engine** | **SigPro** | Atomic reactivity without V-DOM. | | **Engine** | **SigPro** | Atomic reactivity without V-DOM. |
| **Styling** | **Tailwind CSS v4** | Pure CSS performance. | | **Components** | **SigPro-UI** | 60+ semantic, reactive components. |
| **Components** | **daisyUI v5** | Semantic and clean layouts. | | **Styling** | **daisyUI v5** | Beautiful, accessible, themeable. |
| **Learning Curve** | **Zero** | If you know JS and HTML, you know SigPro-UI. | | **Learning Curve** | **Zero** | If you know JS and HTML, you know SigPro-UI. |
### Semantic Functionalism ### Semantic Functionalism
@@ -73,43 +73,43 @@ Modal({
title: "Precision Engineering" title: "Precision Engineering"
}, () => }, () =>
Div({ class: "space-y-4" }, [ Div({ class: "space-y-4" }, [
P("SigPro-UI leverages Tailwind v4 for instant styling."), P("SigPro-UI provides instant reactivity out of the box."),
Button({ Button({
class: "btn-primary", class: "btn-primary",
onclick: () => isVisible(false) onclick: () => isVisible(false)
}, "Confirm") }, "Confirm")
]) ])
) )
```` ```
----- ---
## Technical Stack Requirements ## Technical Stack Requirements
To achieve the performance promised by SigPro-UI, your environment must be equipped with: To achieve the performance promised by SigPro-UI, your environment must be equipped with:
### 1\. SigPro Core ### 1. SigPro Core
The atomic heart. SigPro-UI requires the SigPro runtime (`$`, `$watch`, `$html`, etc.) to be present in the global scope or provided as a module. The atomic heart. SigPro-UI requires the SigPro runtime (`$`, `$watch`, `$html`, etc.) to be present in the global scope or provided as a module.
### 2\. Tailwind CSS v4 Engine ### 2. daisyUI v5
SigPro-UI uses the modern `@theme` and utility engine of Tailwind v4. It is designed to work with the ultra-fast compiler of the new generation.
### 3\. daisyUI v5
The visual DNA. All components are mapped to daisyUI v5 semantic classes, providing access to dozens of themes and accessible UI patterns without writing a single line of custom CSS. The visual DNA. All components are mapped to daisyUI v5 semantic classes, providing access to dozens of themes and accessible UI patterns without writing a single line of custom CSS.
----- ### 3. Modern Browser
SigPro-UI uses modern Web APIs and requires no polyfills for evergreen browsers.
---
<div class="bg-base-200/50 rounded-3xl p-10 my-16 border border-base-300 shadow-inner"> <div class="bg-base-200/50 rounded-3xl p-10 my-16 border border-base-300 shadow-inner">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-center"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-center">
<div class="lg:col-span-2"> <div class="lg:col-span-2">
<h2 class="text-4xl font-black mb-4 mt-0 tracking-tight italic text-secondary">Design at Runtime.</h2> <h2 class="text-4xl font-black mb-4 mt-0 tracking-tight italic text-secondary">Reactive at Runtime.</h2>
<p class="text-xl opacity-80 leading-relaxed"> <p class="text-xl opacity-80 leading-relaxed">
Combine the best of three worlds: <strong>SigPro</strong> for logic, Combine the best of both worlds: <strong>SigPro</strong> for logic and
<strong>Tailwind v4</strong> for speed, and <strong>daisyUI v5</strong> for beauty. <strong>daisyUI v5</strong> for beauty. Build interfaces that feel as fast as they look,
Build interfaces that feel as fast as they look. with components that react instantly to your data changes.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -42,9 +42,4 @@
* [Fieldset](components/fieldset.md) * [Fieldset](components/fieldset.md)
* [Menu](components/menu.md) * [Menu](components/menu.md)
* [Navbar](components/navbar.md) * [Navbar](components/navbar.md)
* [Tabs](components/tabs.md) * [Tabs](components/tabs.md)
* **Advanced**
* [Reactivity Guide](advanced/reactivity.md)
* [i18n Guide](advanced/i18n.md)
* [Theming](advanced/theming.md)

View File

@@ -171,7 +171,7 @@ const CartDemo = () => {
), ),
Span({ class: 'text-lg font-bold' }, () => `Total: $${total()}`) Span({ class: 'text-lg font-bold' }, () => `Total: $${total()}`)
]), ]),
cart().length === 0 () => cart().length === 0
? Div({ class: 'alert alert-soft text-center' }, 'Cart is empty') ? Div({ class: 'alert alert-soft text-center' }, 'Cart is empty')
: Div({ class: 'flex flex-col gap-2' }, cart().map(item => : Div({ class: 'flex flex-col gap-2' }, cart().map(item =>
Div({ class: 'flex justify-between items-center p-2 bg-base-200 rounded-lg' }, [ Div({ class: 'flex justify-between items-center p-2 bg-base-200 rounded-lg' }, [
@@ -187,6 +187,7 @@ const CartDemo = () => {
)) ))
]); ]);
}; };
$mount(CartDemo, '#demo-cart'); $mount(CartDemo, '#demo-cart');
``` ```
@@ -235,7 +236,7 @@ const InboxDemo = () => {
} }
}, 'Mark all read') }, 'Mark all read')
]), ]),
Div({ class: 'flex flex-col gap-2' }, messages().map(msg => Div({ class: 'flex flex-col gap-2' }, () => messages().map(msg =>
Div({ Div({
class: `p-3 rounded-lg cursor-pointer transition-all ${msg.read ? 'bg-base-200 opacity-60' : 'bg-primary/10 border-l-4 border-primary'}`, class: `p-3 rounded-lg cursor-pointer transition-all ${msg.read ? 'bg-base-200 opacity-60' : 'bg-primary/10 border-l-4 border-primary'}`,
onclick: () => markAsRead(msg.id) onclick: () => markAsRead(msg.id)
@@ -247,6 +248,7 @@ const InboxDemo = () => {
)) ))
]); ]);
}; };
$mount(InboxDemo, '#demo-inbox'); $mount(InboxDemo, '#demo-inbox');
``` ```

View File

@@ -125,20 +125,22 @@ $mount(TooltipDemo, '#demo-tooltip');
```javascript ```javascript
const ErrorDemo = () => { const ErrorDemo = () => {
const email = $(''); const email = $('');
const isValid = $(true);
const validate = (value) => { return Div({ class: 'w-full max-w-md' }, [
const valid = value.includes('@') && value.includes('.'); Input({
isValid(valid); type: 'email',
email(value); value: email,
}; placeholder: 'Enter your email',
icon: 'icon-[lucide--mail]',
return Input({ validate: (value) => {
type: 'email', if (!value) return '';
value: email, if (!value.includes('@')) return 'Email must contain @';
error: () => !isValid() && email() ? 'Invalid email address' : '', if (!value.includes('.')) return 'Email must contain .';
oninput: (e) => validate(e.target.value) return '';
}); },
oninput: (e) => email(e.target.value)
})
]);
}; };
$mount(ErrorDemo, '#demo-error'); $mount(ErrorDemo, '#demo-error');
``` ```

View File

@@ -8,23 +8,23 @@ List component with custom item rendering, headers, and reactive data binding.
## Props ## Props
| Prop | Type | Default | Description | | Prop | Type | Default | Description |
| :--- | :--- | :--- | :--- | | :------- | :-------------------------- | :------------------- | :------------------------------------------ |
| `items` | `Array \| Signal<Array>` | `[]` | Data array to display | | `items` | `Array \| Signal<Array>` | `[]` | Data array to display |
| `header` | `string \| VNode \| Signal` | `-` | Optional header content | | `header` | `string \| VNode \| Signal` | `-` | Optional header content |
| `render` | `function(item, index)` | Required | Custom render function for each item | | `render` | `function(item, index)` | Required | Custom render function for each item |
| `keyFn` | `function(item, index)` | `(item, idx) => idx` | Unique key function for items | | `keyFn` | `function(item, index)` | `(item, idx) => idx` | Unique key function for items |
| `class` | `string` | `''` | Additional CSS classes (DaisyUI + Tailwind) | | `class` | `string` | `''` | Additional CSS classes (DaisyUI + Tailwind) |
## Styling ## Styling
List supports all **daisyUI List classes**: List supports all **daisyUI List classes**:
| Category | Keywords | Description | | Category | Keywords | Description |
| :--- | :--- | :--- | | :--------- | :------------ | :------------------------- |
| Base | `list` | Base list styling | | Base | `list` | Base list styling |
| Variant | `list-row` | Row styling for list items | | Variant | `list-row` | Row styling for list items |
| Background | `bg-base-100` | Background color | | Background | `bg-base-100` | Background color |
> For further details, check the [daisyUI List Documentation](https://daisyui.com/components/list) Full reference for CSS classes. > For further details, check the [daisyUI List Documentation](https://daisyui.com/components/list) Full reference for CSS classes.
@@ -41,16 +41,17 @@ List supports all **daisyUI List classes**:
```javascript ```javascript
const BasicDemo = () => { const BasicDemo = () => {
const items = ['Apple', 'Banana', 'Orange', 'Grape', 'Mango']; const items = ["Apple", "Banana", "Orange", "Grape", "Mango"];
return List({ return List({
items: items, items: items,
render: (item) => Div({ class: 'p-3 hover:bg-base-200 transition-colors' }, [ render: (item) =>
Span({ class: 'font-medium' }, item) Div({ class: "p-3 hover:bg-base-200 transition-colors" }, [
]) Span({ class: "font-medium" }, item),
]),
}); });
}; };
$mount(BasicDemo, '#demo-basic'); $mount(BasicDemo, "#demo-basic");
``` ```
### With Header ### With Header
@@ -65,22 +66,31 @@ $mount(BasicDemo, '#demo-basic');
```javascript ```javascript
const HeaderDemo = () => { const HeaderDemo = () => {
const users = [ const users = [
{ name: 'John Doe', email: 'john@example.com', status: 'active' }, { name: "John Doe", email: "john@example.com", status: "active" },
{ name: 'Jane Smith', email: 'jane@example.com', status: 'inactive' }, { name: "Jane Smith", email: "jane@example.com", status: "inactive" },
{ name: 'Bob Johnson', email: 'bob@example.com', status: 'active' } { name: "Bob Johnson", email: "bob@example.com", status: "active" },
]; ];
return List({ return List({
items: users, items: users,
header: Div({ class: 'p-3 bg-primary/10 font-bold border-b border-base-300' }, 'Active Users'), header: Div(
render: (user) => Div({ class: 'p-3 border-b border-base-300 hover:bg-base-200' }, [ { class: "p-3 bg-primary/10 font-bold border-b border-base-300" },
Div({ class: 'font-medium' }, user.name), "Active Users",
Div({ class: 'text-sm opacity-70' }, user.email), ),
Span({ class: `badge badge-sm ${user.status === 'active' ? 'badge-success' : 'badge-ghost'} mt-1` }, user.status) render: (user) =>
]) Div({ class: "p-3 border-b border-base-300 hover:bg-base-200" }, [
Div({ class: "font-medium" }, user.name),
Div({ class: "text-sm opacity-70" }, user.email),
Span(
{
class: `badge badge-sm ${user.status === "active" ? "badge-success" : "badge-ghost"} mt-1`,
},
user.status,
),
]),
}); });
}; };
$mount(HeaderDemo, '#demo-header'); $mount(HeaderDemo, "#demo-header");
``` ```
### With Icons ### With Icons
@@ -95,25 +105,36 @@ $mount(HeaderDemo, '#demo-header');
```javascript ```javascript
const IconsDemo = () => { const IconsDemo = () => {
const settings = [ const settings = [
{ icon: '🔊', label: 'Sound', description: 'Adjust volume and notifications' }, {
{ icon: '🌙', label: 'Display', description: 'Brightness and dark mode' }, icon: "🔊",
{ icon: '🔒', label: 'Privacy', description: 'Security settings' }, label: "Sound",
{ icon: '🌐', label: 'Network', description: 'WiFi and connections' } description: "Adjust volume and notifications",
},
{ icon: "🌙", label: "Display", description: "Brightness and dark mode" },
{ icon: "🔒", label: "Privacy", description: "Security settings" },
{ icon: "🌐", label: "Network", description: "WiFi and connections" },
]; ];
return List({ return List({
items: settings, items: settings,
render: (item) => Div({ class: 'flex gap-3 p-3 hover:bg-base-200 transition-colors cursor-pointer' }, [ render: (item) =>
Div({ class: 'text-2xl' }, item.icon), Div(
Div({ class: 'flex-1' }, [ {
Div({ class: 'font-medium' }, item.label), class:
Div({ class: 'text-sm opacity-60' }, item.description) "flex gap-3 p-3 hover:bg-base-200 transition-colors cursor-pointer",
]), },
Span({ class: 'opacity-40' }, '→') [
]) Div({ class: "text-2xl" }, item.icon),
Div({ class: "flex-1" }, [
Div({ class: "font-medium" }, item.label),
Div({ class: "text-sm opacity-60" }, item.description),
]),
Span({ class: "opacity-40" }, "→"),
],
),
}); });
}; };
$mount(IconsDemo, '#demo-icons'); $mount(IconsDemo, "#demo-icons");
``` ```
### With Badges ### With Badges
@@ -128,24 +149,52 @@ $mount(IconsDemo, '#demo-icons');
```javascript ```javascript
const BadgesDemo = () => { const BadgesDemo = () => {
const notifications = [ const notifications = [
{ id: 1, message: 'New comment on your post', time: '5 min ago', unread: true }, {
{ id: 2, message: 'Your order has been shipped', time: '1 hour ago', unread: true }, id: 1,
{ id: 3, message: 'Welcome to the platform!', time: '2 days ago', unread: false }, message: "New comment on your post",
{ id: 4, message: 'Weekly digest available', time: '3 days ago', unread: false } time: "5 min ago",
unread: true,
},
{
id: 2,
message: "Your order has been shipped",
time: "1 hour ago",
unread: true,
},
{
id: 3,
message: "Welcome to the platform!",
time: "2 days ago",
unread: false,
},
{
id: 4,
message: "Weekly digest available",
time: "3 days ago",
unread: false,
},
]; ];
return List({ return List({
items: notifications, items: notifications,
render: (item) => Div({ class: `flex justify-between items-center p-3 border-b border-base-300 hover:bg-base-200 ${item.unread ? 'bg-primary/5' : ''}` }, [ render: (item) =>
Div({ class: 'flex-1' }, [ Div(
Div({ class: 'font-medium' }, item.message), {
Div({ class: 'text-xs opacity-50' }, item.time) class: `flex justify-between items-center p-3 border-b border-base-300 hover:bg-base-200 ${item.unread ? "bg-primary/5" : ""}`,
]), },
item.unread ? Span({ class: 'badge badge-primary badge-sm' }, 'New') : null [
]) Div({ class: "flex-1" }, [
Div({ class: "font-medium" }, item.message),
Div({ class: "text-xs opacity-50" }, item.time),
]),
item.unread
? Span({ class: "badge badge-primary badge-sm" }, "New")
: null,
],
),
}); });
}; };
$mount(BadgesDemo, '#demo-badges'); $mount(BadgesDemo, "#demo-badges");
``` ```
### Interactive List ### Interactive List
@@ -161,38 +210,49 @@ $mount(BadgesDemo, '#demo-badges');
const InteractiveDemo = () => { const InteractiveDemo = () => {
const selected = $(null); const selected = $(null);
const items = [ const items = [
{ id: 1, name: 'Project Alpha', status: 'In Progress' }, { id: 1, name: "Project Alpha", status: "In Progress" },
{ id: 2, name: 'Project Beta', status: 'Planning' }, { id: 2, name: "Project Beta", status: "Planning" },
{ id: 3, name: 'Project Gamma', status: 'Completed' }, { id: 3, name: "Project Gamma", status: "Completed" },
{ id: 4, name: 'Project Delta', status: 'Review' } { id: 4, name: "Project Delta", status: "Review" },
]; ];
const statusColors = { const statusColors = {
'In Progress': 'badge-warning', "In Progress": "badge-warning",
'Planning': 'badge-info', Planning: "badge-info",
'Completed': 'badge-success', Completed: "badge-success",
'Review': 'badge-accent' Review: "badge-accent",
}; };
return Div({ class: 'flex flex-col gap-4' }, [ return Div({ class: "flex flex-col gap-4" }, [
List({ List({
items: items, items: items,
render: (item) => Div({ render: (item) =>
class: `p-3 cursor-pointer transition-all hover:bg-base-200 ${selected() === item.id ? 'bg-primary/10 border-l-4 border-primary' : 'border-l-4 border-transparent'}`, Div(
onclick: () => selected(item.id) {
}, [ class: `p-3 cursor-pointer transition-all hover:bg-base-200 ${selected() === item.id ? "bg-primary/10 border-l-4 border-primary" : "border-l-4 border-transparent"}`,
Div({ class: 'flex justify-between items-center' }, [ onclick: () => selected(item.id),
Div({ class: 'font-medium' }, item.name), },
Span({ class: `badge ${statusColors[item.status]}` }, item.status) [
]) Div({ class: "flex justify-between items-center" }, [
]) Div({ class: "font-medium" }, item.name),
Span(
{ class: `badge ${statusColors[item.status]}` },
item.status,
),
]),
],
),
}), }),
() => selected() () =>
? Div({ class: 'alert alert-info' }, `Selected: ${items.find(i => i.id === selected()).name}`) selected()
: Div({ class: 'alert alert-soft' }, 'Select a project to see details') ? Div(
{ class: "alert alert-info" },
`Selected: ${items.find((i) => i.id === selected()).name}`,
)
: Div({ class: "alert alert-soft" }, "Select a project to see details"),
]); ]);
}; };
$mount(InteractiveDemo, '#demo-interactive'); $mount(InteractiveDemo, "#demo-interactive");
``` ```
### Reactive List (Todo App) ### Reactive List (Todo App)
@@ -223,9 +283,7 @@ const ReactiveDemo = () => {
}; };
const toggleTodo = (id) => { const toggleTodo = (id) => {
todos(todos().map(t => todos(todos().map(t => t.id === id ? { ...t, done: !t.done } : t));
t.id === id ? { ...t, done: !t.done } : t
));
}; };
const deleteTodo = (id) => { const deleteTodo = (id) => {
@@ -233,13 +291,12 @@ const ReactiveDemo = () => {
}; };
const pendingCount = () => todos().filter(t => !t.done).length; const pendingCount = () => todos().filter(t => !t.done).length;
$watch(()=> console.log(pendingCount()));
return Div({ class: 'flex flex-col gap-4' }, [ return Div({ class: 'flex flex-col gap-4' }, [
Div({ class: 'flex gap-2' }, [ Div({ class: 'flex gap-2' }, [
Input({ Input({
placeholder: 'Add new task...', placeholder: 'Add new task...',
value: newTodo, value: newTodo,
class: 'flex-1',
oninput: (e) => newTodo(e.target.value), oninput: (e) => newTodo(e.target.value),
onkeypress: (e) => e.key === 'Enter' && addTodo() onkeypress: (e) => e.key === 'Enter' && addTodo()
}), }),
@@ -247,24 +304,32 @@ const ReactiveDemo = () => {
]), ]),
List({ List({
items: todos, items: todos,
render: (todo) => Div({ class: `flex items-center gap-3 p-2 border-b border-base-300 hover:bg-base-200 ${todo.done ? 'opacity-60' : ''}` }, [ render: (item) => {
Checkbox({ // Esta función busca siempre el estado actual del item dentro del signal
value: todo.done, const it = () => todos().find(t => t.id === item.id) || item;
onclick: () => toggleTodo(todo.id)
}), return Div({
Span({ class: () => `flex items-center gap-3 p-2 border-b border-base-300 ${it().done ? 'opacity-60' : ''}`
class: `flex-1 ${todo.done ? 'line-through' : ''}`, }, [
onclick: () => toggleTodo(todo.id) Checkbox({
}, todo.text), value: () => it().done,
Button({ onclick: () => toggleTodo(item.id)
class: 'btn btn-ghost btn-xs btn-circle', }),
onclick: () => deleteTodo(todo.id) Span({
}, '✕') class: () => `flex-1 ${it().done ? 'line-through' : ''}`,
]) onclick: () => toggleTodo(item.id)
}, () => it().text),
Button({
class: 'btn btn-ghost btn-xs btn-circle',
onclick: () => deleteTodo(item.id)
}, '✕')
]);
}
}), }),
Div({ class: 'text-sm opacity-70 mt-2' }, () => `${pendingCount()} tasks remaining`) Div({ class: 'text-sm opacity-70 mt-2' }, () => `${pendingCount()} tasks remaining`)
]); ]);
}; };
$mount(ReactiveDemo, '#demo-reactive'); $mount(ReactiveDemo, '#demo-reactive');
``` ```
@@ -280,27 +345,45 @@ $mount(ReactiveDemo, '#demo-reactive');
```javascript ```javascript
const AvatarDemo = () => { const AvatarDemo = () => {
const contacts = [ const contacts = [
{ name: 'Alice Johnson', role: 'Developer', avatar: 'A', online: true }, { name: "Alice Johnson", role: "Developer", avatar: "A", online: true },
{ name: 'Bob Smith', role: 'Designer', avatar: 'B', online: false }, { name: "Bob Smith", role: "Designer", avatar: "B", online: false },
{ name: 'Charlie Brown', role: 'Manager', avatar: 'C', online: true }, { name: "Charlie Brown", role: "Manager", avatar: "C", online: true },
{ name: 'Diana Prince', role: 'QA Engineer', avatar: 'D', online: false } { name: "Diana Prince", role: "QA Engineer", avatar: "D", online: false },
]; ];
return List({ return List({
items: contacts, items: contacts,
render: (contact) => Div({ class: 'flex gap-3 p-3 hover:bg-base-200 transition-colors' }, [ render: (contact) =>
Div({ class: `avatar ${contact.online ? 'online' : 'offline'}`, style: 'width: 48px' }, [ Div({ class: "flex gap-3 p-3 hover:bg-base-200 transition-colors" }, [
Div({ class: 'rounded-full bg-primary text-primary-content flex items-center justify-center w-12 h-12 font-bold' }, contact.avatar) Div(
{
class: `avatar ${contact.online ? "online" : "offline"}`,
style: "width: 48px",
},
[
Div(
{
class:
"rounded-full bg-primary text-primary-content flex items-center justify-center w-12 h-12 font-bold",
},
contact.avatar,
),
],
),
Div({ class: "flex-1" }, [
Div({ class: "font-medium" }, contact.name),
Div({ class: "text-sm opacity-60" }, contact.role),
]),
Div(
{
class: `badge badge-sm ${contact.online ? "badge-success" : "badge-ghost"}`,
},
contact.online ? "Online" : "Offline",
),
]), ]),
Div({ class: 'flex-1' }, [
Div({ class: 'font-medium' }, contact.name),
Div({ class: 'text-sm opacity-60' }, contact.role)
]),
Div({ class: `badge badge-sm ${contact.online ? 'badge-success' : 'badge-ghost'}` }, contact.online ? 'Online' : 'Offline')
])
}); });
}; };
$mount(AvatarDemo, '#demo-avatar'); $mount(AvatarDemo, "#demo-avatar");
``` ```
### All Variants ### All Variants
@@ -314,29 +397,29 @@ $mount(AvatarDemo, '#demo-avatar');
```javascript ```javascript
const VariantsDemo = () => { const VariantsDemo = () => {
const items = ['Item 1', 'Item 2', 'Item 3']; const items = ["Item 1", "Item 2", "Item 3"];
return Div({ class: 'flex flex-col gap-6' }, [ return Div({ class: "flex flex-col gap-6" }, [
Div({ class: 'text-sm font-bold' }, 'Default List'), Div({ class: "text-sm font-bold" }, "Default List"),
List({ List({
items: items, items: items,
render: (item) => Div({ class: 'p-2' }, item) render: (item) => Div({ class: "p-2" }, item),
}), }),
Div({ class: 'text-sm font-bold mt-2' }, 'With Shadow'), Div({ class: "text-sm font-bold mt-2" }, "With Shadow"),
List({ List({
items: items, items: items,
render: (item) => Div({ class: 'p-2' }, item), render: (item) => Div({ class: "p-2" }, item),
class: 'shadow-lg' class: "shadow-lg",
}), }),
Div({ class: 'text-sm font-bold mt-2' }, 'Rounded Corners'), Div({ class: "text-sm font-bold mt-2" }, "Rounded Corners"),
List({ List({
items: items, items: items,
render: (item) => Div({ class: 'p-2' }, item), render: (item) => Div({ class: "p-2" }, item),
class: 'rounded-box overflow-hidden' class: "rounded-box overflow-hidden",
}) }),
]); ]);
}; };
$mount(VariantsDemo, '#demo-variants'); $mount(VariantsDemo, "#demo-variants");
``` ```

View File

@@ -140,41 +140,6 @@ const ReactiveDemo = () => {
$mount(ReactiveDemo, '#demo-reactive'); $mount(ReactiveDemo, '#demo-reactive');
``` ```
### With Trend Indicators
<div class="card bg-base-200 border border-base-300 shadow-sm my-6">
<div class="card-body">
<h3 class="card-title text-sm uppercase opacity-50 mb-4">Live Demo</h3>
<div id="demo-trends" class="bg-base-100 p-6 rounded-xl border border-base-300 grid grid-cols-1 md:grid-cols-3 gap-4"></div>
</div>
</div>
```javascript
const TrendsDemo = () => {
return Div({ class: 'grid grid-cols-1 md:grid-cols-3 gap-4' }, [
Stat({
label: 'Weekly Sales',
value: '$12,345',
desc: Div({ class: 'text-success' }, '↗︎ 15% increase'),
icon: Span({ class: 'text-2xl' }, '📈')
}),
Stat({
label: 'Bounce Rate',
value: '42%',
desc: Div({ class: 'text-error' }, '↘︎ 3% from last week'),
icon: Span({ class: 'text-2xl' }, '📉')
}),
Stat({
label: 'Avg. Session',
value: '4m 32s',
desc: Div({ class: 'text-warning' }, '↗︎ 12 seconds'),
icon: Span({ class: 'text-2xl' }, '⏱️')
})
]);
};
$mount(TrendsDemo, '#demo-trends');
```
### Multiple Stats in Row ### Multiple Stats in Row
<div class="card bg-base-200 border border-base-300 shadow-sm my-6"> <div class="card bg-base-200 border border-base-300 shadow-sm my-6">

View File

@@ -294,27 +294,22 @@ $mount(ColorsDemo, '#demo-colors');
const AllPositionsDemo = () => { const AllPositionsDemo = () => {
return Div({ class: 'grid grid-cols-3 gap-4 justify-items-center' }, [ return Div({ class: 'grid grid-cols-3 gap-4 justify-items-center' }, [
Div({ class: 'col-start-2' }, [ Div({ class: 'col-start-2' }, [
Tooltip({ tip: 'Top tooltip', class: 'tooltip-top' }, [ Tooltip({ tip: 'Top tooltip', ui: 'tooltip-top' }, [
Button({ class: 'btn btn-sm w-24' }, 'Top') Button({ class: 'btn btn-sm w-24' }, 'Top')
]) ])
]), ]),
Div({ class: 'col-start-1 row-start-2' }, [ Div({ class: 'col-start-1 row-start-2' }, [
Tooltip({ tip: 'Left tooltip', class: 'tooltip-left' }, [ Tooltip({ tip: 'Left tooltip', ui: 'tooltip-left' }, [
Button({ class: 'btn btn-sm w-24' }, 'Left') Button({ class: 'btn btn-sm w-24' }, 'Left')
]) ])
]), ]),
Div({ class: 'col-start-2 row-start-2' }, [
Tooltip({ tip: 'Center tooltip', class: 'tooltip' }, [
Button({ class: 'btn btn-sm w-24' }, 'Center')
])
]),
Div({ class: 'col-start-3 row-start-2' }, [ Div({ class: 'col-start-3 row-start-2' }, [
Tooltip({ tip: 'Right tooltip', class: 'tooltip-right' }, [ Tooltip({ tip: 'Right tooltip', ui: 'tooltip-right' }, [
Button({ class: 'btn btn-sm w-24' }, 'Right') Button({ class: 'btn btn-sm w-24' }, 'Right')
]) ])
]), ]),
Div({ class: 'col-start-2 row-start-3' }, [ Div({ class: 'col-start-2 row-start-3' }, [
Tooltip({ tip: 'Bottom tooltip', class: 'tooltip-bottom' }, [ Tooltip({ tip: 'Bottom tooltip', ui: 'tooltip-bottom' }, [
Button({ class: 'btn btn-sm w-24' }, 'Bottom') Button({ class: 'btn btn-sm w-24' }, 'Bottom')
]) ])
]) ])

View File

@@ -6,7 +6,7 @@ Follow these steps to integrate **SigPro-UI** into your project.
!> **📘 Core Concepts** !> **📘 Core Concepts**
**Note:** SigPro-UI now includes SigPro core internally. You no longer need to install SigPro separately. **Note:** SigPro-UI now includes SigPro core internally. No need to install SigPro separately.
SigProUI is built on top of the [SigPro](https://natxocc.github.io/sigpro/#/) reactive core. To learn how to create signals, manage reactivity, and structure your application logic, check out the [SigPro documentation](https://natxocc.github.io/sigpro/#/). It covers everything you need to build reactive applications with signals, computed values, and effects. SigProUI is built on top of the [SigPro](https://natxocc.github.io/sigpro/#/) reactive core. To learn how to create signals, manage reactivity, and structure your application logic, check out the [SigPro documentation](https://natxocc.github.io/sigpro/#/). It covers everything you need to build reactive applications with signals, computed values, and effects.
--- ---
@@ -135,7 +135,6 @@ When you install SigProUI, you get:
- And 30+ more components! - And 30+ more components!
### Utilities ### Utilities
- `Utils` - Helper functions (ui, val)
- `tt()` - i18n translation function - `tt()` - i18n translation function
## Language Support ## Language Support
@@ -143,7 +142,7 @@ When you install SigProUI, you get:
SigProUI includes built-in i18n with Spanish and English: SigProUI includes built-in i18n with Spanish and English:
```javascript ```javascript
import { tt } from 'sigpro-ui'; import { tt, Locale } from 'sigpro-ui';
// Change locale (default is 'es') // Change locale (default is 'es')
Locale('en'); Locale('en');

View File

@@ -1,6 +1,5 @@
# SigPro-UI Quick Reference # SigPro-UI Quick Reference
**Version:** daisyUI v5 + Tailwind v4 Plugin
**Status:** Active / WIP **Status:** Active / WIP
@@ -8,9 +7,7 @@
```javascript ```javascript
import "sigpro-ui"; import "sigpro-ui";
import "sigpro-ui/css";
// Injects all components into window and sets default language
Locale('en'); // 'es' or 'en'
// All components (Button, Input, Table, Toast, etc.) are now globally available. // All components (Button, Input, Table, Toast, etc.) are now globally available.
``` ```
@@ -22,7 +19,7 @@ Locale('en'); // 'es' or 'en'
| Component | Purpose | Basic Example | | Component | Purpose | Basic Example |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Button** | Styled button with DaisyUI | `Button({ class: "btn-primary" }, "Submit")` | | **Button** | Styled button with DaisyUI | `Button({ class: "btn-primary" }, "Submit")` |
| **Input** | Reactive text field with floating label | `Input({ label: "Name", value: $name })` | | **Input** | Reactive text field with validation | `Input({ value: $name, validate: (v) => !v ? "Required" : "" })` |
| **Select** | Dropdown selection menu | `Select({ options: ["Admin", "User"], value: $role })` | | **Select** | Dropdown selection menu | `Select({ options: ["Admin", "User"], value: $role })` |
| **Checkbox** | Binary toggle (boolean) | `Checkbox({ label: "Active", checked: $isActive })` | | **Checkbox** | Binary toggle (boolean) | `Checkbox({ label: "Active", checked: $isActive })` |
| **Table** | Data grid with column rendering | `Table({ items: $data, columns: [...] })` | | **Table** | Data grid with column rendering | `Table({ items: $data, columns: [...] })` |
@@ -40,7 +37,7 @@ Locale('en'); // 'es' or 'en'
| Component | Description | Example | | Component | Description | Example |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Input** | Text input with floating label, validation, password toggle | `Input({ label: "Email", type: "email", value: $email })` | | **Input** | Text input with floating label, validation, password toggle | `Input({ label: "Email", type: "email", value: $email, validate: validateEmail })` |
| **Select** | Dropdown selector | `Select({ label: "Role", options: ["Admin", "User"], value: $role })` | | **Select** | Dropdown selector | `Select({ label: "Role", options: ["Admin", "User"], value: $role })` |
| **Autocomplete** | Searchable dropdown with filtering | `Autocomplete({ label: "Country", options: countryList, value: $country })` | | **Autocomplete** | Searchable dropdown with filtering | `Autocomplete({ label: "Country", options: countryList, value: $country })` |
| **Datepicker** | Date picker (single or range mode) | `Datepicker({ label: "Date", value: $date, range: false })` | | **Datepicker** | Date picker (single or range mode) | `Datepicker({ label: "Date", value: $date, range: false })` |
@@ -53,6 +50,36 @@ Locale('en'); // 'es' or 'en'
--- ---
## Input Validation
The `Input` component supports real-time validation via the `validate` prop:
```javascript
const email = $('');
Input({
type: 'email',
value: email,
placeholder: 'Enter your email',
icon: 'icon-[lucide--mail]',
validate: (value) => {
if (!value) return '';
if (!value.includes('@')) return 'Email must contain @';
if (!value.includes('.')) return 'Email must contain .';
return '';
},
oninput: (e) => email(e.target.value)
})
```
**How it works:**
- Returns `''` or `null` → no error
- Returns a string → shows error message and adds `input-error` class
- Validates on every keystroke
- No external state needed for error messages
---
## Data Display ## Data Display
| Component | Description | Example | | Component | Description | Example |
@@ -63,7 +90,7 @@ Locale('en'); // 'es' or 'en'
| **Stat** | Statistical data blocks (KPIs) | `Stat({ label: "Total", value: "1.2k", desc: "Monthly" })` | | **Stat** | Statistical data blocks (KPIs) | `Stat({ label: "Total", value: "1.2k", desc: "Monthly" })` |
| **Timeline** | Vertical/horizontal timeline | `Timeline({ items: [{ title: "Step 1", detail: "Completed" }] })` | | **Timeline** | Vertical/horizontal timeline | `Timeline({ items: [{ title: "Step 1", detail: "Completed" }] })` |
| **Stack** | Stacked elements | `Stack({}, [Card1, Card2, Card3])` | | **Stack** | Stacked elements | `Stack({}, [Card1, Card2, Card3])` |
| **Indicator** | Badge on corner of element | `Indicator({ badge: "3" }, Button(...))` | | **Indicator** | Badge on corner of element | `Indicator({ value: () => count() }, Button(...))` |
--- ---
@@ -74,7 +101,7 @@ Locale('en'); // 'es' or 'en'
| **Alert** | Inline contextual notification | `Alert({ type: "success" }, "Changes saved!")` | | **Alert** | Inline contextual notification | `Alert({ type: "success" }, "Changes saved!")` |
| **Modal** | Dialog overlay | `Modal({ open: $isOpen, title: "Confirm" }, "Are you sure?")` | | **Modal** | Dialog overlay | `Modal({ open: $isOpen, title: "Confirm" }, "Are you sure?")` |
| **Toast** | Floating notification (auto-stacking) | `Toast("Action completed", "alert-info", 3000)` | | **Toast** | Floating notification (auto-stacking) | `Toast("Action completed", "alert-info", 3000)` |
| **Tooltip** | Hover tooltip wrapper | `Tooltip({ tip: "Help text" }, Button(...))` | | **Tooltip** | Hover tooltip wrapper | `Tooltip({ tip: "Help text", ui: "tooltip-top" }, Button(...))` |
--- ---
@@ -97,7 +124,7 @@ Locale('en'); // 'es' or 'en'
| Component | Description | Example | | Component | Description | Example |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **Fab** | Floating Action Button with actions | `Fab({ icon: "+", actions: [{ label: "Add", onclick: add }] })` | | **Fab** | Floating Action Button with actions | `Fab({ icon: "+", actions: [{ label: "Add", onclick: add }] })` |
| **Indicator** | Badge indicator wrapper | `Indicator({ badge: "99+" }, Button(...))` | | **Indicator** | Badge indicator wrapper | `Indicator({ value: () => unread() }, Button(...))` |
--- ---
@@ -138,15 +165,16 @@ const closeText = tt("close"); // "Close" or "Cerrar"
```javascript ```javascript
const name = $(""); const name = $("");
const error = $(null);
Input({ Input({
value: name, value: name,
error: error, placeholder: "Name",
oninput: (e) => { validate: (value) => {
name(e.target.value); if (!value) return "Name is required";
error(e.target.value.length < 3 ? "Name too short" : null); if (value.length < 3) return "Name too short";
} return "";
},
oninput: (e) => name(e.target.value)
}) })
``` ```
@@ -186,14 +214,14 @@ Modal({
| Component | Key Props | | Component | Key Props |
| :--- | :--- | | :--- | :--- |
| `Button` | `class`, `disabled`, `loading`, `badge`, `tooltip`, `icon` | | `Button` | `class`, `disabled`, `loading`, `icon` |
| `Input` | `label`, `value`, `error`, `type`, `placeholder`, `disabled`, `tip` | | `Input` | `value`, `validate`, `type`, `placeholder`, `icon`, `disabled` |
| `Select` | `label`, `options`, `value`, `disabled` | | `Select` | `label`, `options`, `value`, `disabled` |
| `Modal` | `open`, `title`, `buttons` | | `Modal` | `open`, `title`, `buttons` |
| `Table` | `items`, `columns`, `zebra`, `pinRows`, `empty` | | `Table` | `items`, `columns`, `zebra`, `pinRows`, `empty` |
| `Alert` | `type` (info/success/warning/error), `soft`, `actions` | | `Alert` | `type` (info/success/warning/error), `soft`, `actions` |
| `Toast` | `message`, `type`, `duration` | | `Toast` | `message`, `type`, `duration` |
| `Loading` | `show` |
| `Datepicker` | `value`, `range`, `label`, `placeholder` | | `Datepicker` | `value`, `range`, `label`, `placeholder` |
| `Autocomplete` | `options`, `value`, `onSelect`, `label` | | `Autocomplete` | `options`, `value`, `onSelect`, `label` |
| `Indicator` | `value` (function that returns number/string) |
| `Tooltip` | `tip`, `ui` (tooltip-top/bottom/left/right) |

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
// index.js // index.js
import 'sigpro'; import './src/sigpro.js';
// import './src/css/sigpro.css'; // No importes CSS en JS // import './src/css/sigpro.css'; // No importes CSS en JS
import * as Components from './src/components/index.js'; import * as Components from './src/components/index.js';
// import * as Icons from './src/core/icons.js'; // ELIMINAR // import * as Icons from './src/core/icons.js'; // ELIMINAR

View File

@@ -1,12 +1,15 @@
{ {
"name": "sigpro-ui", "name": "sigpro-ui",
"version": "1.1.0", "version": "1.1.0",
"type": "module",
"license": "MIT",
"main": "./index.js", "main": "./index.js",
"module": "./index.js", "module": "./index.js",
"unpkg": "./dist/sigpro-ui.min.js", "devDependencies": {
"jsdelivr": "./dist/sigpro-ui.min.js", "@iconify/json": "^2.2.458",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.0.0",
"daisyui": "^5.5.19",
"tailwindcss": "^4.2.2"
},
"exports": { "exports": {
".": { ".": {
"import": "./index.js", "import": "./index.js",
@@ -24,6 +27,8 @@
"README.md", "README.md",
"LICENSE" "LICENSE"
], ],
"jsdelivr": "./dist/sigpro-ui.min.js",
"license": "MIT",
"scripts": { "scripts": {
"build:cssmin": "./node_modules/.bin/tailwindcss -i ./src/css/sigpro.css -o ./css/sigpro.min.css --content './src/**/*.js' --minify", "build:cssmin": "./node_modules/.bin/tailwindcss -i ./src/css/sigpro.css -o ./css/sigpro.min.css --content './src/**/*.js' --minify",
"build:css": "./node_modules/.bin/tailwindcss -i ./src/css/sigpro.css -o ./css/sigpro.css --content './src/**/*.js'", "build:css": "./node_modules/.bin/tailwindcss -i ./src/css/sigpro.css -o ./css/sigpro.css --content './src/**/*.js'",
@@ -34,14 +39,6 @@
"prepublishOnly": "bun run build", "prepublishOnly": "bun run build",
"docs": "bun x serve docs" "docs": "bun x serve docs"
}, },
"dependencies": { "type": "module",
"sigpro": "^1.1.18" "unpkg": "./dist/sigpro-ui.min.js"
},
"devDependencies": {
"@iconify/json": "^2.2.458",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.0.0",
"daisyui": "^5.5.19",
"tailwindcss": "^4.2.2"
}
} }

View File

@@ -1,5 +1,5 @@
// components/Accordion.js // components/Accordion.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui, val } from "../core/utils.js"; import { ui, val } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Alert.js // components/Alert.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui, getIcon } from "../core/utils.js"; import { ui, getIcon } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Autocomplete.js // components/Autocomplete.js
import { $, $html, $for } from "sigpro"; import { $, $html, $for } from "../sigpro.js";
import { val } from "../core/utils.js"; import { val } from "../core/utils.js";
import { tt } from "../core/i18n.js"; import { tt } from "../core/i18n.js";
import { Input } from "./Input.js"; import { Input } from "./Input.js";

View File

@@ -1,5 +1,5 @@
// components/Badge.js // components/Badge.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Button.js // components/Button.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui, val, getIcon } from "../core/utils.js"; import { ui, val, getIcon } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Checkbox.js // components/Checkbox.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Colorpicker.js // components/Colorpicker.js
import { $, $html, $if } from "sigpro"; import { $, $html, $if } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Datepicker.js // components/Datepicker.js
import { $, $html, $if } from "sigpro"; import { $, $html, $if } from "../sigpro.js";
import { val, ui, getIcon } from "../core/utils.js"; import { val, ui, getIcon } from "../core/utils.js";
import { Input } from "./Input.js"; import { Input } from "./Input.js";

View File

@@ -1,5 +1,5 @@
// components/Drawer.js // components/Drawer.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Dropdown.js // components/Dropdown.js
// import { $html, $for, $watch } from "sigpro"; // import { $html, $for, $watch } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Fab.js // components/Fab.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { val, ui, getIcon } from "../core/utils.js"; import { val, ui, getIcon } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Fieldset.js // components/Fieldset.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Fileinput.js // components/Fileinput.js
import { $, $html, $if, $for } from "sigpro"; import { $, $html, $if, $for } from "../sigpro.js";
import { ui, getIcon } from "../core/utils.js"; import { ui, getIcon } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Indicator.js // components/Indicator.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Input.js // components/Input.js
import { $, $html } from "sigpro"; import { $, $html, $watch } from "../sigpro.js";
import { val, ui, getIcon } from "../core/utils.js"; import { val, ui, getIcon } from "../core/utils.js";
/** /**
@@ -20,12 +20,14 @@ export const Input = (props) => {
oninput, oninput,
placeholder, placeholder,
disabled, disabled,
size, // para poder pasar el tamaño también al botón size,
validate,
...rest ...rest
} = props; } = props;
const isPassword = type === "password"; const isPassword = type === "password";
const visible = $(false); const visible = $(false);
const errorMsg = $(null);
const iconMap = { const iconMap = {
text: "icon-[lucide--text]", text: "icon-[lucide--text]",
@@ -39,7 +41,6 @@ export const Input = (props) => {
}; };
const leftIcon = icon ? getIcon(icon) : (iconMap[type] ? getIcon(iconMap[type]) : null); const leftIcon = icon ? getIcon(icon) : (iconMap[type] ? getIcon(iconMap[type]) : null);
const getPasswordIcon = () => getIcon(visible() ? "icon-[lucide--eye-off]" : "icon-[lucide--eye]"); const getPasswordIcon = () => getIcon(visible() ? "icon-[lucide--eye-off]" : "icon-[lucide--eye]");
const paddingLeft = leftIcon ? "pl-10" : ""; const paddingLeft = leftIcon ? "pl-10" : "";
@@ -52,25 +53,43 @@ export const Input = (props) => {
return 'btn-md'; return 'btn-md';
}; };
const handleInput = (e) => {
const newValue = e.target.value;
if (validate) {
const result = validate(newValue);
errorMsg(result || null);
}
oninput?.(e);
};
const hasError = () => errorMsg() && errorMsg() !== '';
const inputClasses = () => {
let classes = `input w-full ${paddingLeft} ${paddingRight}`;
if (className) classes += ` ${className}`;
if (hasError()) classes += ' input-error';
return classes.trim();
};
const inputElement = $html("input", {
...rest,
type: () => (isPassword ? (visible() ? "text" : "password") : type),
placeholder: placeholder || " ",
class: inputClasses,
value: value,
oninput: handleInput,
disabled: () => val(disabled),
"aria-invalid": () => hasError() ? "true" : "false",
});
return $html( return $html(
"div", "div",
{ class: "relative w-full" }, { class: "relative w-full" },
() => [ () => [
// Input inputElement,
$html("input", {
...rest,
type: () => (isPassword ? (visible() ? "text" : "password") : type),
placeholder: placeholder || " ",
class: ui('input w-full', `${paddingLeft} ${paddingRight} ${className || ''}`.trim()),
value: value,
oninput: (e) => oninput?.(e),
disabled: () => val(disabled),
}),
leftIcon ? $html("div", { leftIcon ? $html("div", {
class: "absolute left-3 inset-y-0 flex items-center pointer-events-none text-base-content/60", class: "absolute left-3 inset-y-0 flex items-center pointer-events-none text-base-content/60",
}, leftIcon) : null, }, leftIcon) : null,
isPassword ? $html("button", { isPassword ? $html("button", {
type: "button", type: "button",
class: ui( class: ui(
@@ -83,6 +102,9 @@ export const Input = (props) => {
visible(!visible()); visible(!visible());
} }
}, () => getPasswordIcon()) : null, }, () => getPasswordIcon()) : null,
$html("div", {
class: "text-error text-xs mt-1 px-3 absolute -bottom-5 left-0",
}, () => hasError() ? errorMsg() : null),
] ]
); );
}; };

View File

@@ -1,5 +1,5 @@
// components/Label.js // components/Label.js
import { $, $html } from "sigpro"; import { $, $html } from "../sigpro.js";
import { ui, val } from "../core/utils.js"; import { ui, val } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/List.js // components/List.js
import { $html, $if, $for } from "sigpro"; import { $html, $if, $for } from "../sigpro.js";
import { ui, val } from "../core/utils.js"; import { ui, val } from "../core/utils.js";
/** /**
@@ -12,7 +12,7 @@ import { ui, val } from "../core/utils.js";
* - flex, items-center, gap-2 * - flex, items-center, gap-2
*/ */
export const List = (props) => { export const List = (props) => {
const { class: className, items, header, render, keyFn = (item, index) => index, ...rest } = props; const { class: className, items, header, render, keyFn = (item, index) => item.id ?? index, ...rest } = props;
const listItems = $for( const listItems = $for(
items, items,
@@ -20,12 +20,8 @@ export const List = (props) => {
keyFn keyFn
); );
return $html( return $html("ul", {
"ul", ...rest,
{ class: ui('list bg-base-100 rounded-box shadow-md', className),
...rest, }, header ? [$if(header, () => $html("li", { class: "p-4 pb-2 text-xs opacity-60" }, [val(header)])), listItems] : listItems);
class: ui('list bg-base-100 rounded-box shadow-md', className),
},
header ? [$if(header, () => $html("li", { class: "p-4 pb-2 text-xs opacity-60" }, [val(header)])), listItems] : listItems
);
}; };

View File

@@ -1,5 +1,5 @@
// components/Menu.js // components/Menu.js
import { $html, $for } from "sigpro"; import { $html, $for } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Modal.js // components/Modal.js
import { $html, $watch } from "sigpro"; import { $html, $watch } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
import { tt } from "../core/i18n.js"; import { tt } from "../core/i18n.js";
import { Button } from "./Button.js"; import { Button } from "./Button.js";

View File

@@ -1,5 +1,5 @@
// components/Navbar.js // components/Navbar.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Radio.js // components/Radio.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Range.js // components/Range.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Rating.js // components/Rating.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Select.js // components/Select.js
import { $html, $for } from "sigpro"; import { $html, $for } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Stack.js // components/Stack.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Stat.js // components/Stat.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Swap.js // components/Swap.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui, val } from "../core/utils.js"; import { ui, val } from "../core/utils.js";
/** /**

View File

@@ -1,5 +1,5 @@
// components/Table.js // components/Table.js
import { $html, $for, $if } from "sigpro"; import { $html, $for, $if } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
import { tt } from "../core/i18n.js"; import { tt } from "../core/i18n.js";
@@ -22,6 +22,8 @@ export const Table = (props) => {
return ui('table', className, zebraClass, pinRowsClass); return ui('table', className, zebraClass, pinRowsClass);
}; };
const getInternalKeyFn = keyFn || ((item, idx) => item.id || idx);
return $html("div", { class: "overflow-x-auto w-full bg-base-100 rounded-box border border-base-300" }, [ return $html("div", { class: "overflow-x-auto w-full bg-base-100 rounded-box border border-base-300" }, [
$html("table", { ...rest, class: tableClass }, [ $html("table", { ...rest, class: tableClass }, [
$html("thead", {}, [ $html("thead", {}, [
@@ -31,17 +33,24 @@ export const Table = (props) => {
]), ]),
$html("tbody", {}, [ $html("tbody", {}, [
$for(items, (item, index) => { $for(items, (item, index) => {
const it = () => {
const currentItems = val(items);
const key = getInternalKeyFn(item, index);
return currentItems.find((u, i) => getInternalKeyFn(u, i) === key) || item;
};
return $html("tr", { class: "hover" }, return $html("tr", { class: "hover" },
columns.map(col => { columns.map(col => {
const cellContent = () => { const cellContent = () => {
if (col.render) return col.render(item, index); const latestItem = it();
const value = item[col.key]; if (col.render) return col.render(latestItem, index);
return val(value); return val(latestItem[col.key]);
}; };
return $html("td", { class: col.class || "" }, [cellContent]); return $html("td", { class: col.class || "" }, [cellContent]);
}) })
); );
}, keyFn || ((item, idx) => item.id || idx)), }, getInternalKeyFn),
$if(() => val(items).length === 0, () => $if(() => val(items).length === 0, () =>
$html("tr", {}, [ $html("tr", {}, [
@@ -50,14 +59,7 @@ export const Table = (props) => {
]) ])
]) ])
) )
]), ])
$if(() => columns.some(c => c.footer), () =>
$html("tfoot", {}, [
$html("tr", {},
columns.map(col => $html("th", {}, col.footer || ""))
)
])
)
]) ])
]); ]);
}; };

View File

@@ -1,5 +1,5 @@
// components/Tabs.js // components/Tabs.js
import { $, $html, $for } from "sigpro"; import { $, $html, $for } from "../sigpro.js";
import { val, ui } from "../core/utils.js"; import { val, ui } from "../core/utils.js";
/** /**
@@ -13,58 +13,63 @@ import { val, ui } from "../core/utils.js";
export const Tabs = (props) => { export const Tabs = (props) => {
const { items, class: className, ...rest } = props; const { items, class: className, ...rest } = props;
const itemsSignal = typeof items === "function" ? items : () => items || []; const itemsSignal = typeof items === "function" ? items : () => items || [];
const name = `tabs-${Math.random().toString(36).slice(2, 9)}`; const activeIndex = $(0);
// Encontrar el índice activo $watch(() => {
const getActiveIndex = () => { const idx = itemsSignal().findIndex(it => val(it.active) === true);
const arr = itemsSignal(); if (idx !== -1 && idx !== activeIndex()) activeIndex(idx);
const idx = arr.findIndex(it => val(it.active) === true); });
return idx === -1 ? 0 : idx;
};
const activeIndex = $(getActiveIndex); return $html("div", { ...rest, class: "w-full" }, [
// 1. Tab List: Aplanamos los botones para que sean hijos directos
const updateActiveIndex = () => { $html("div", {
const newIndex = getActiveIndex(); role: "tablist",
if (newIndex !== activeIndex()) activeIndex(newIndex); class: ui('tabs', className || 'tabs-box')
}; }, () => {
const list = itemsSignal();
$watch(() => updateActiveIndex()); return list.map((it, idx) => {
const isSelected = () => activeIndex() === idx;
return $html("div", {
...rest, const tab = $html("button", {
class: ui('tabs', className || 'tabs-box') role: "tab",
}, [ class: () => ui("tab", isSelected() ? "tab-active" : ""),
$for(itemsSignal, (it, idx) => { onclick: (e) => {
const isChecked = () => activeIndex() === idx; e.preventDefault();
const getLabelText = () => { if (!val(it.disabled)) {
const label = typeof it.label === "function" ? it.label() : it.label;
return typeof label === "string" ? label : `Tab ${idx + 1}`;
};
return [
$html("input", {
type: "radio",
name: name,
class: "tab",
"aria-label": getLabelText(),
checked: isChecked, // ← función reactiva, no string hardcodeado
disabled: () => val(it.disabled),
onchange: (e) => {
if (e.target.checked && !val(it.disabled)) {
if (it.onclick) it.onclick(); if (it.onclick) it.onclick();
if (typeof it.active === "function") it.active(true);
activeIndex(idx); activeIndex(idx);
} }
} }
}), });
$html("div", {
// Mantenemos el watch para el label por si es dinámico
$watch(() => {
const content = val(it.label);
if (content instanceof Node) {
tab.replaceChildren(content);
} else {
tab.textContent = String(content);
}
});
return tab;
});
}),
// 2. Tab Content: Aquí el display:contents no molesta tanto,
// pero lo aplanamos por consistencia
$html("div", { class: "tab-panels" }, () => {
return itemsSignal().map((it, idx) => {
const isVisible = () => activeIndex() === idx;
return $html("div", {
role: "tabpanel",
class: "tab-content bg-base-100 border-base-300 p-6", class: "tab-content bg-base-100 border-base-300 p-6",
style: () => isChecked() ? "display: block" : "display: none" style: () => isVisible() ? "display: block" : "display: none"
}, [ }, [
typeof it.content === "function" ? it.content() : it.content () => typeof it.content === "function" ? it.content() : it.content
]) ]);
]; });
}, (it, idx) => idx) })
]); ]);
}; };

View File

@@ -1,5 +1,5 @@
// components/Timeline.js // components/Timeline.js
import { $html, $for } from "sigpro"; import { $html, $for } from "../sigpro.js";
import { val, ui, getIcon } from "../core/utils.js"; import { val, ui, getIcon } from "../core/utils.js";
/** /**
@@ -21,38 +21,39 @@ export const Timeline = (props) => {
error: "icon-[lucide--alert-circle]", error: "icon-[lucide--alert-circle]",
}; };
const itemsSource = typeof items === "function" ? items : () => items || [];
return $html( return $html(
"ul", "ul",
{ {
...rest, ...rest,
class: () => ui( class: () => ui(
`timeline ${val(vertical) ? "timeline-vertical" : "timeline-horizontal"} ${val(compact) ? "timeline-compact" : ""}`, className), `timeline ${val(vertical) ? "timeline-vertical" : "timeline-horizontal"} ${val(compact) ? "timeline-compact" : ""}`,
}, className
[
$for(
itemsSource,
(item, i) => {
const isFirst = i === 0;
const isLast = i === itemsSource().length - 1;
const itemType = item.type || "success";
const renderSlot = (content) => (typeof content === "function" ? content() : content);
return $html("li", { class: "flex-1" }, [
!isFirst ? $html("hr", { class: () => item.completed ? "bg-primary" : "" }) : null,
$html("div", { class: "timeline-start" }, () => renderSlot(item.title)),
$html("div", { class: "timeline-middle" }, () => [
item.icon
? getIcon(item.icon)
: getIcon(iconMap[itemType] || iconMap.success)
]),
$html("div", { class: "timeline-end timeline-box shadow-sm" }, () => renderSlot(item.detail)),
!isLast ? $html("hr", { class: () => item.completed ? "bg-primary" : "" }) : null,
]);
},
(item, i) => item.id || i,
), ),
], },
() => {
const list = (typeof items === "function" ? items() : items) || [];
return list.map((item, i) => {
const isFirst = i === 0;
const isLast = i === list.length - 1;
const itemType = item.type || "success";
const isCompleted = () => val(item.completed);
// Nueva lógica: La línea de entrada se pinta si el ANTERIOR estaba completado
const prevCompleted = () => i > 0 && val(list[i - 1].completed);
const renderSlot = (content) => (typeof content === "function" ? content() : content);
return $html("li", { class: "flex-1" }, [
!isFirst ? $html("hr", { class: () => prevCompleted() ? "bg-primary" : "" }) : null,
$html("div", { class: "timeline-start" }, [() => renderSlot(item.title)]),
$html("div", { class: "timeline-middle" }, [
() => item.icon ? getIcon(item.icon) : getIcon(iconMap[itemType] || iconMap.success)
]),
$html("div", { class: "timeline-end timeline-box shadow-sm" }, [() => renderSlot(item.detail)]),
!isLast ? $html("hr", { class: () => isCompleted() ? "bg-primary" : "" }) : null,
]);
});
}
); );
}; };

View File

@@ -1,5 +1,5 @@
// components/Toast.js // components/Toast.js
import { $html, $mount } from "sigpro"; import { $html, $mount } from "../sigpro.js";
import { getIcon } from "../core/utils.js"; import { getIcon } from "../core/utils.js";
import { Button } from "./Button.js"; import { Button } from "./Button.js";

View File

@@ -1,5 +1,5 @@
// components/Tooltip.js // components/Tooltip.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
import { ui } from "../core/utils.js"; import { ui } from "../core/utils.js";
/** /**

View File

@@ -1,4 +1,4 @@
import { $ } from "sigpro"; import { $ } from "../sigpro.js";
export const i18n = { export const i18n = {
es: { es: {

View File

@@ -1,5 +1,5 @@
// core/utils.js // core/utils.js
import { $html } from "sigpro"; import { $html } from "../sigpro.js";
export const val = t => typeof t === "function" ? t() : t; export const val = t => typeof t === "function" ? t() : t;

482
src/sigpro.js Normal file
View File

@@ -0,0 +1,482 @@
/**
* SigPro Core
*/
let activeEffect = null;
let currentOwner = null;
const effectQueue = new Set();
let isFlushing = false;
const MOUNTED_NODES = new WeakMap();
/** flush */
const flush = () => {
if (isFlushing) return;
isFlushing = true;
while (effectQueue.size > 0) {
const sorted = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
effectQueue.clear();
for (const eff of sorted) if (!eff._deleted) eff();
}
isFlushing = false;
};
/** track */
const track = (subs) => {
if (activeEffect && !activeEffect._deleted) {
subs.add(activeEffect);
activeEffect._deps.add(subs);
}
};
/** trigger */
const trigger = (subs) => {
for (const eff of subs) {
if (eff === activeEffect || eff._deleted) continue;
if (eff._isComputed) {
eff.markDirty();
if (eff._subs) trigger(eff._subs);
} else {
effectQueue.add(eff);
}
}
if (!isFlushing) queueMicrotask(flush);
};
/** sweep */
const sweep = (node) => {
if (node._cleanups) {
node._cleanups.forEach((f) => f());
node._cleanups.clear();
}
node.childNodes?.forEach(sweep);
};
/** _view */
const _view = (fn) => {
const cleanups = new Set();
const prev = currentOwner;
const container = document.createElement("div");
container.style.display = "contents";
currentOwner = { cleanups };
try {
const res = fn({ onCleanup: (f) => cleanups.add(f) });
const process = (n) => {
if (!n) return;
if (n._isRuntime) {
cleanups.add(n.destroy);
container.appendChild(n.container);
} else if (Array.isArray(n)) n.forEach(process);
else container.appendChild(n instanceof Node ? n : document.createTextNode(String(n)));
};
process(res);
} finally { currentOwner = prev; }
return {
_isRuntime: true,
container,
destroy: () => {
cleanups.forEach((f) => f());
sweep(container);
container.remove();
},
};
};
/**
* Creates a reactive Signal or a Computed Value.
* @param {any|Function} initial - Initial value or a getter function for computed state.
* @param {string} [key] - Optional. Key for automatic persistence in localStorage.
* @returns {Function} Signal getter/setter. Use `sig()` to read and `sig(val)` to write.
* @example
* const count = $(0); // Simple signal
* const double = $(() => count() * 2); // Computed signal
* const name = $("John", "user-name"); // Persisted signal
*/
const $ = (initial, key = null) => {
if (typeof initial === "function") {
const subs = new Set();
let cached, dirty = true;
const effect = () => {
if (effect._deleted) return;
effect._deps.forEach((s) => s.delete(effect));
effect._deps.clear();
const prev = activeEffect;
activeEffect = effect;
try {
const val = initial();
if (!Object.is(cached, val) || dirty) {
cached = val;
dirty = false;
trigger(subs);
}
} finally { activeEffect = prev; }
};
effect._deps = new Set();
effect._isComputed = true;
effect._subs = subs;
effect._deleted = false;
effect.markDirty = () => (dirty = true);
effect.stop = () => {
effect._deleted = true;
effect._deps.forEach((s) => s.delete(effect));
subs.clear();
};
if (currentOwner) currentOwner.cleanups.add(effect.stop);
return () => { if (dirty) effect(); track(subs); return cached; };
}
let value = initial;
if (key) {
try {
const saved = localStorage.getItem(key);
if (saved !== null) value = JSON.parse(saved);
} catch (e) {
console.warn("SigPro: LocalStorage locked", e);
}
}
const subs = new Set();
return (...args) => {
if (args.length) {
const next = typeof args[0] === "function" ? args[0](value) : args[0];
if (!Object.is(value, next)) {
value = next;
if (key) localStorage.setItem(key, JSON.stringify(value));
trigger(subs);
}
}
track(subs);
return value;
};
};
/**
* Watches for signal changes and executes a side effect.
* Handles automatic cleanup of previous effects.
* @param {Function|Array} target - Function to execute or Array of signals for explicit dependency tracking.
* @param {Function} [fn] - If the first parameter is an Array, this is the callback function.
* @returns {Function} Function to manually stop the watcher.
* @example
* $watch(() => console.log("Count is:", count()));
* $watch([count], () => console.log("Only runs when count changes"));
*/
const $watch = (target, fn) => {
const isExplicit = Array.isArray(target);
const callback = isExplicit ? fn : target;
const depsInput = isExplicit ? target : null;
if (typeof callback !== "function") return () => { };
const owner = currentOwner;
const runner = () => {
if (runner._deleted) return;
runner._deps.forEach((s) => s.delete(runner));
runner._deps.clear();
runner._cleanups.forEach((c) => c());
runner._cleanups.clear();
const prevEffect = activeEffect;
const prevOwner = currentOwner;
activeEffect = runner;
currentOwner = { cleanups: runner._cleanups };
runner.depth = prevEffect ? prevEffect.depth + 1 : 0;
try {
if (isExplicit) {
activeEffect = null;
callback();
activeEffect = runner;
depsInput.forEach(d => typeof d === "function" && d());
} else {
callback();
}
} finally {
activeEffect = prevEffect;
currentOwner = prevOwner;
}
};
runner._deps = new Set();
runner._cleanups = new Set();
runner._deleted = false;
runner.stop = () => {
if (runner._deleted) return;
runner._deleted = true;
effectQueue.delete(runner);
runner._deps.forEach((s) => s.delete(runner));
runner._cleanups.forEach((c) => c());
if (owner) owner.cleanups.delete(runner.stop);
};
if (owner) owner.cleanups.add(runner.stop);
runner();
return runner.stop;
};
/**
* DOM element rendering engine with built-in reactivity.
* @param {string} tag - HTML tag name (e.g., 'div', 'span').
* @param {Object} [props] - Attributes, events (onEvent), or two-way bindings (value, checked).
* @param {Array|any} [content] - Children: text, other nodes, or reactive signals.
* @returns {HTMLElement} The configured reactive DOM element.
*/
const $html = (tag, props = {}, content = []) => {
if (props instanceof Node || Array.isArray(props) || typeof props !== "object") {
content = props; props = {};
}
const el = document.createElement(tag),
_sanitize = (key, val) => (key === 'src' || key === 'href') && String(val).toLowerCase().includes('javascript:') ? '#' : val;
el._cleanups = new Set();
const boolAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
for (let [key, val] of Object.entries(props)) {
if (key === "ref") { (typeof val === "function" ? val(el) : (val.current = el)); continue; }
const isSignal = typeof val === "function",
isInput = ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName),
isBindAttr = (key === "value" || key === "checked");
if (isInput && isBindAttr && isSignal) {
el._cleanups.add($watch(() => { const currentVal = val(); if (el[key] !== currentVal) el[key] = currentVal; }));
const eventName = key === "checked" ? "change" : "input", handler = (event) => val(event.target[key]);
el.addEventListener(eventName, handler);
el._cleanups.add(() => el.removeEventListener(eventName, handler));
} else if (key.startsWith("on")) {
const eventName = key.slice(2).toLowerCase().split(".")[0], handler = (event) => val(event);
el.addEventListener(eventName, handler);
el._cleanups.add(() => el.removeEventListener(eventName, handler));
} else if (isSignal) {
el._cleanups.add($watch(() => {
const currentVal = _sanitize(key, val());
if (key === "class") {
el.className = currentVal || "";
} else if (boolAttrs.includes(key)) {
if (currentVal) {
el.setAttribute(key, "");
el[key] = true;
} else {
el.removeAttribute(key);
el[key] = false;
}
} else {
currentVal == null ? el.removeAttribute(key) : el.setAttribute(key, currentVal);
}
}));
} else {
if (boolAttrs.includes(key)) {
if (val) {
el.setAttribute(key, "");
el[key] = true;
} else {
el.removeAttribute(key);
el[key] = false;
}
} else {
el.setAttribute(key, _sanitize(key, val));
}
}
}
const append = (child) => {
if (Array.isArray(child)) return child.forEach(append);
if (child instanceof Node) {
el.appendChild(child);
} else if (typeof child === "function") {
const marker = document.createTextNode("");
el.appendChild(marker);
let nodes = [];
el._cleanups.add($watch(() => {
const res = child(), next = (Array.isArray(res) ? res : [res]).map((i) =>
i?._isRuntime ? i.container : i instanceof Node ? i : document.createTextNode(i ?? "")
);
nodes.forEach((n) => { sweep?.(n); n.remove(); });
next.forEach((n) => marker.parentNode?.insertBefore(n, marker));
nodes = next;
}));
} else el.appendChild(document.createTextNode(child ?? ""));
};
append(content);
return el;
};
/**
* Conditional rendering component.
* @param {Function|boolean} condition - Reactive signal or boolean value.
* @param {Function|HTMLElement} thenVal - Content to show if true.
* @param {Function|HTMLElement} [otherwiseVal] - Content to show if false (optional).
* @returns {HTMLElement} A reactive container (display: contents).
*/
const $if = (condition, thenVal, otherwiseVal = null) => {
const marker = document.createTextNode("");
const container = $html("div", { style: "display:contents" }, [marker]);
let current = null, last = null;
$watch(() => {
const state = !!(typeof condition === "function" ? condition() : condition);
if (state !== last) {
last = state;
if (current) current.destroy();
const branch = state ? thenVal : otherwiseVal;
if (branch) {
current = _view(() => typeof branch === "function" ? branch() : branch);
container.insertBefore(current.container, marker);
}
}
});
return container;
};
$if.not = (condition, thenVal, otherwiseVal) => $if(() => !(typeof condition === "function" ? condition() : condition), thenVal, otherwiseVal);
/**
* Optimized reactive loop with key-based reconciliation.
* @param {Function|Array} source - Signal containing an Array of data.
* @param {Function} render - Function receiving (item, index) and returning a node.
* @param {Function} keyFn - Function to extract a unique key from the item.
* @returns {HTMLElement} A reactive container (display: contents).
*/
const $for = (source, render, keyFn, tag = "div", props = { style: "display:contents" }) => {
const marker = document.createTextNode("");
const container = $html(tag, props, [marker]);
let cache = new Map();
$watch(() => {
const items = (typeof source === "function" ? source() : source) || [];
const newCache = new Map();
const newOrder = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const key = keyFn ? keyFn(item, i) : i;
let run = cache.get(key);
if (!run) {
run = _view(() => render(item, i));
} else {
cache.delete(key);
}
newCache.set(key, run);
newOrder.push(key);
}
cache.forEach(run => {
run.destroy();
run.container.remove();
});
let anchor = marker;
for (let i = newOrder.length - 1; i >= 0; i--) {
const run = newCache.get(newOrder[i]);
if (run.container.nextSibling !== anchor) {
container.insertBefore(run.container, anchor);
}
anchor = run.container;
}
cache = newCache;
});
return container;
};
/**
* Hash-based (#) routing system.
* @param {Array<{path: string, component: Function}>} routes - Route definitions.
* @returns {HTMLElement} The router outlet container.
*/
const $router = (routes) => {
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
const outlet = $html("div", { class: "router-outlet" });
let current = null;
$watch([sPath], async () => {
const path = sPath();
const route = routes.find(r => {
const rp = r.path.split("/").filter(Boolean), pp = path.split("/").filter(Boolean);
return rp.length === pp.length && rp.every((p, i) => p.startsWith(":") || p === pp[i]);
}) || routes.find(r => r.path === "*");
if (route) {
let comp = route.component;
if (typeof comp === "function" && comp.toString().includes('import')) {
comp = (await comp()).default || (await comp());
}
const params = {};
route.path.split("/").filter(Boolean).forEach((p, i) => {
if (p.startsWith(":")) params[p.slice(1)] = path.split("/").filter(Boolean)[i];
});
if (current) current.destroy();
if ($router.params) $router.params(params);
current = _view(() => {
try {
return typeof comp === "function" ? comp(params) : comp;
} catch (e) {
return $html("div", { class: "p-4 text-error" }, "Error loading view");
}
});
outlet.appendChild(current.container);
}
});
return outlet;
};
$router.params = $({});
$router.to = (path) => (window.location.hash = path.replace(/^#?\/?/, "#/"));
$router.back = () => window.history.back();
$router.path = () => window.location.hash.replace(/^#/, "") || "/";
/**
* Mounts a component or node into a DOM target element.
* It automatically handles the cleanup of any previously mounted SigPro instances
* in that target to prevent memory leaks and duplicate renders.
* * @param {Function|HTMLElement} component - The component function to render or a pre-built DOM node.
* @param {string|HTMLElement} target - A CSS selector string or a direct DOM element to mount into.
* @returns {Object|undefined} The view instance containing the `container` and `destroy` method, or undefined if target is not found.
* * @example
* // Mount using a component function
* $mount(() => Div({ class: "app" }, "Hello World"), "#root");
* * // Mount using a direct element
* const myApp = Div("Hello");
* $mount(myApp, document.getElementById("app"));
*/
const $mount = (component, target) => {
const el = typeof target === "string" ? document.querySelector(target) : target;
if (!el) return;
if (MOUNTED_NODES.has(el)) MOUNTED_NODES.get(el).destroy();
const instance = _view(typeof component === "function" ? component : () => component);
el.replaceChildren(instance.container);
MOUNTED_NODES.set(el, instance);
return instance;
};
/** GLOBAL CORE REGISTRY */
const SigProCore = { $, $watch, $html, $if, $for, $router, $mount };
if (typeof window !== "undefined") {
const install = (registry) => {
Object.keys(registry).forEach(key => {
window[key] = registry[key];
});
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((tagName) => {
const helperName = tagName.charAt(0).toUpperCase() + tagName.slice(1);
if (!(helperName in window)) {
window[helperName] = (props, content) => $html(tagName, props, content);
}
});
window.SigPro = Object.freeze(registry);
};
install(SigProCore);
}
export { $, $watch, $html, $if, $for, $router, $mount };
export default SigProCore;