1.2.12 Fix computed problem
This commit is contained in:
@@ -16,7 +16,4 @@
|
||||
* [Tag](api/html.md)
|
||||
* [Tags](api/tags.md)
|
||||
* [Global Store](api/global.md)
|
||||
* [JSX Style](api/jsx.md)
|
||||
|
||||
* **UI Components**
|
||||
* [Quick Start](ui/quick.md)
|
||||
* [JSX Style](api/jsx.md)
|
||||
@@ -134,94 +134,4 @@ const PersistDemo = () => {
|
||||
]);
|
||||
};
|
||||
Mount(PersistDemo, '#demo-persist');
|
||||
```
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const initExamples = () => {
|
||||
|
||||
const counterTarget = document.querySelector('#demo-counter');
|
||||
if (counterTarget && !counterTarget.hasChildNodes()) {
|
||||
const Counter = () => {
|
||||
const $count = $(0);
|
||||
return Div({ class: 'flex gap-4 items-center' }, [
|
||||
Button({ class: 'btn btn-circle btn-outline', onclick: () => $count(c => c - 1) }, "-"),
|
||||
Span({ class: 'text-2xl font-mono w-12 text-center' }, $count),
|
||||
Button({ class: 'btn btn-circle btn-primary', onclick: () => $count(c => c + 1) }, "+")
|
||||
]);
|
||||
};
|
||||
Mount(Counter, counterTarget);
|
||||
}
|
||||
|
||||
// 2. Computed
|
||||
const computedTarget = document.querySelector('#demo-computed');
|
||||
if (computedTarget && !computedTarget.hasChildNodes()) {
|
||||
const ComputedDemo = () => {
|
||||
const $count = $(10);
|
||||
const $double = $(() => $count() * 2);
|
||||
return Div({ class: 'space-y-4 w-full' }, [
|
||||
Input({ type: 'range', min: 1, max: 100, class: 'range range-primary', value: $count }),
|
||||
P({ class: 'text-center' }, ["Base: ", $count, " ⮕ ", Span({class: 'text-primary font-bold'}, ["Double: ", $double])])
|
||||
]);
|
||||
};
|
||||
Mount(ComputedDemo, computedTarget);
|
||||
}
|
||||
|
||||
// 3. List
|
||||
const listTarget = document.querySelector('#demo-list');
|
||||
if (listTarget && !listTarget.hasChildNodes()) {
|
||||
const ListDemo = () => {
|
||||
const $todos = $(['Learn SigPro', 'Build an App']);
|
||||
const $input = $("");
|
||||
const addTodo = () => { if ($input()) { $todos(prev => [...prev, $input()]); $input(""); } };
|
||||
return Div([
|
||||
Div({ class: 'flex gap-2 mb-4' }, [
|
||||
Input({ class: 'input input-bordered flex-1', value: $input, placeholder: 'New task...' }),
|
||||
Button({ class: 'btn btn-primary', onclick: addTodo }, "Add")
|
||||
]),
|
||||
Ul({ class: 'menu bg-base-200 rounded-box p-2' }, For($todos, (item) => Li([A(item)]), (item) => item))
|
||||
]);
|
||||
};
|
||||
Mount(ListDemo, listTarget);
|
||||
}
|
||||
|
||||
// 4. If
|
||||
const ifTarget = document.querySelector('#demo-if');
|
||||
if (ifTarget && !ifTarget.hasChildNodes()) {
|
||||
const ConditionalDemo = () => {
|
||||
const $isVisible = $(false);
|
||||
return Div({ class: 'text-center w-full' }, [
|
||||
Button({ class: 'btn btn-outline mb-4', onclick: () => $isVisible(! $isVisible()) }, "Toggle Secret"),
|
||||
If($isVisible,
|
||||
() => Div({ class: 'p-4 bg-warning text-warning-content rounded-lg' }, "🤫 SigPro is Awesome!"),
|
||||
() => Div({ class: 'p-4 opacity-50' }, "Nothing to see here...")
|
||||
)
|
||||
]);
|
||||
};
|
||||
Mount(ConditionalDemo, ifTarget);
|
||||
}
|
||||
|
||||
// 5. Persist
|
||||
const persistTarget = document.querySelector('#demo-persist');
|
||||
if (persistTarget && !persistTarget.hasChildNodes()) {
|
||||
const PersistDemo = () => {
|
||||
const $name = $("Guest", "sigpro-demo-name");
|
||||
return Div({ class: 'flex flex-col gap-2' }, [
|
||||
H3({ class: 'text-lg font-bold' }, ["Hello, ", $name]),
|
||||
Input({ class: 'input input-bordered', value: $name }),
|
||||
P({ class: 'text-xs opacity-50' }, "Refresh the page!")
|
||||
]);
|
||||
};
|
||||
Mount(PersistDemo, persistTarget);
|
||||
}
|
||||
};
|
||||
|
||||
// Ejecutar inmediatamente y también en cada navegación de Docsify
|
||||
initExamples();
|
||||
if (window.$docsify) {
|
||||
window.$docsify.plugins = [].concat(window.$docsify.plugins || [], (hook) => {
|
||||
hook.doneEach(initExamples);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
@@ -1,38 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SigPro Docs</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>SigPro Docs</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'SigPro',
|
||||
repo: '',
|
||||
loadSidebar: true,
|
||||
subMaxLevel: 0,
|
||||
sidebarDisplayLevel: 1,
|
||||
executeScript: true,
|
||||
copyCode: {
|
||||
buttonText: '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
|
||||
errorText: 'Error',
|
||||
successText: '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg>'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdn.jsdelivr.net/npm/docsify/lib/themes/vue.css"
|
||||
/>
|
||||
|
||||
<script src="./sigpro.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/daisyui@5"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: "SigPro",
|
||||
repo: "",
|
||||
loadSidebar: true,
|
||||
subMaxLevel: 0,
|
||||
sidebarDisplayLevel: 1,
|
||||
executeScript: true,
|
||||
copyCode: {
|
||||
buttonText:
|
||||
'<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
|
||||
errorText: "Error",
|
||||
successText:
|
||||
'<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg>',
|
||||
},
|
||||
plugins: [
|
||||
function (hook, vm) {
|
||||
hook.doneEach(function () {
|
||||
const codeBlocks = document.querySelectorAll(
|
||||
'pre[data-lang="javascript"] code',
|
||||
);
|
||||
|
||||
codeBlocks.forEach((code) => {
|
||||
try {
|
||||
const runDemo = new Function(code.innerText);
|
||||
runDemo();
|
||||
} catch (err) {
|
||||
console.error("Error en la demo de SigPro:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="./sigpro.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
140
docs/sigpro.js
140
docs/sigpro.js
@@ -40,6 +40,7 @@
|
||||
If: () => If,
|
||||
For: () => For,
|
||||
Batch: () => Batch,
|
||||
Anim: () => Anim,
|
||||
$$: () => $$,
|
||||
$: () => $
|
||||
});
|
||||
@@ -154,11 +155,11 @@
|
||||
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||
subs.add(activeEffect);
|
||||
(activeEffect._deps ||= new Set).add(subs);
|
||||
} else if (trigger) {
|
||||
} else if (trigger && subs.size > 0) {
|
||||
let hasQueue = false;
|
||||
subs.forEach((e) => {
|
||||
for (const e of subs) {
|
||||
if (e === activeEffect || e._disposed)
|
||||
return;
|
||||
continue;
|
||||
if (e._isComputed) {
|
||||
e._dirty = true;
|
||||
if (e._subs)
|
||||
@@ -167,29 +168,29 @@
|
||||
effectQueue.add(e);
|
||||
hasQueue = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (hasQueue && !isFlushing && batchDepth === 0)
|
||||
queueMicrotask(flush);
|
||||
}
|
||||
};
|
||||
var $ = (val, key = null) => {
|
||||
var $ = (val2, key = null) => {
|
||||
const subs = new Set;
|
||||
if (isFunc(val)) {
|
||||
let cache, dirty = true;
|
||||
if (isFunc(val2)) {
|
||||
let cache;
|
||||
const computed = () => {
|
||||
if (dirty) {
|
||||
if (computed._dirty) {
|
||||
const prev = activeEffect;
|
||||
activeEffect = computed;
|
||||
try {
|
||||
const next = val();
|
||||
const next = val2();
|
||||
if (!Object.is(cache, next)) {
|
||||
cache = next;
|
||||
dirty = false;
|
||||
trackUpdate(subs, true);
|
||||
}
|
||||
} finally {
|
||||
activeEffect = prev;
|
||||
}
|
||||
computed._dirty = false;
|
||||
}
|
||||
trackUpdate(subs);
|
||||
return cache;
|
||||
@@ -199,44 +200,35 @@
|
||||
computed._dirty = true;
|
||||
computed._deps = null;
|
||||
computed._disposed = false;
|
||||
computed.markDirty = () => {
|
||||
dirty = true;
|
||||
};
|
||||
computed.stop = () => {
|
||||
computed._disposed = true;
|
||||
if (computed._deps) {
|
||||
computed._deps.forEach((depSet) => depSet.delete(computed));
|
||||
computed._deps.clear();
|
||||
}
|
||||
subs.clear();
|
||||
};
|
||||
computed.stop = () => {};
|
||||
if (activeOwner)
|
||||
onUnmount(computed.stop);
|
||||
return computed;
|
||||
}
|
||||
if (key)
|
||||
try {
|
||||
val = JSON.parse(localStorage.getItem(key)) ?? val;
|
||||
val2 = JSON.parse(localStorage.getItem(key)) ?? val2;
|
||||
} catch (e) {}
|
||||
return (...args) => {
|
||||
if (args.length) {
|
||||
const next = isFunc(args[0]) ? args[0](val) : args[0];
|
||||
if (!Object.is(val, next)) {
|
||||
val = next;
|
||||
const next = isFunc(args[0]) ? args[0](val2) : args[0];
|
||||
if (!Object.is(val2, next)) {
|
||||
val2 = next;
|
||||
if (key)
|
||||
localStorage.setItem(key, JSON.stringify(val));
|
||||
localStorage.setItem(key, JSON.stringify(val2));
|
||||
trackUpdate(subs, true);
|
||||
}
|
||||
}
|
||||
trackUpdate(subs);
|
||||
return val;
|
||||
return val2;
|
||||
};
|
||||
};
|
||||
var $$ = (target) => {
|
||||
if (!isObj(target))
|
||||
return target;
|
||||
if (proxyCache.has(target))
|
||||
return proxyCache.get(target);
|
||||
let proxy = proxyCache.get(target);
|
||||
if (proxy)
|
||||
return proxy;
|
||||
const subsMap = new Map;
|
||||
const getSubs = (k) => {
|
||||
let s = subsMap.get(k);
|
||||
@@ -244,28 +236,30 @@
|
||||
subsMap.set(k, s = new Set);
|
||||
return s;
|
||||
};
|
||||
const proxy = new Proxy(target, {
|
||||
get(t, k) {
|
||||
trackUpdate(getSubs(k));
|
||||
return $$(t[k]);
|
||||
proxy = new Proxy(target, {
|
||||
get(t, k, receiver) {
|
||||
if (typeof k !== "symbol")
|
||||
trackUpdate(getSubs(k));
|
||||
return $$(Reflect.get(t, k, receiver));
|
||||
},
|
||||
set(t, k, v) {
|
||||
const isNew = !(k in t);
|
||||
if (!Object.is(t[k], v)) {
|
||||
t[k] = v;
|
||||
set(t, k, v, receiver) {
|
||||
const isNew = !Reflect.has(t, k);
|
||||
const oldV = Reflect.get(t, k, receiver);
|
||||
const result = Reflect.set(t, k, v, receiver);
|
||||
if (result && !Object.is(oldV, v)) {
|
||||
trackUpdate(getSubs(k), true);
|
||||
if (isNew)
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return true;
|
||||
return result;
|
||||
},
|
||||
deleteProperty(t, k) {
|
||||
const res = Reflect.deleteProperty(t, k);
|
||||
if (res) {
|
||||
const result = Reflect.deleteProperty(t, k);
|
||||
if (result) {
|
||||
trackUpdate(getSubs(k), true);
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return res;
|
||||
return result;
|
||||
},
|
||||
ownKeys(t) {
|
||||
trackUpdate(getSubs(ITER));
|
||||
@@ -300,17 +294,17 @@
|
||||
};
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var isDangerousAttr = (key) => key === "src" || key === "href" || key.startsWith("on");
|
||||
var validateAttr = (key, val) => {
|
||||
if (val == null || val === false)
|
||||
var validateAttr = (key, val2) => {
|
||||
if (val2 == null || val2 === false)
|
||||
return null;
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val);
|
||||
const sVal = String(val2);
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
return val;
|
||||
return val2;
|
||||
};
|
||||
var Tag = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
@@ -342,7 +336,7 @@
|
||||
isArr(node) ? node.forEach(attach) : attach(node);
|
||||
return node;
|
||||
}
|
||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
||||
const isSVG = /^(svg|path|circle|rect|line|poly(line|gon)|g|defs|text(path)?|tspan|use|symbol|image|marker|ellipse)$/i.test(tag);
|
||||
const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag);
|
||||
el._cleanups = new Set;
|
||||
for (let k in props) {
|
||||
@@ -353,6 +347,11 @@
|
||||
isFunc(v) ? v(el) : v.current = el;
|
||||
continue;
|
||||
}
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
const ns = "http://www.w3.org/1999/xlink";
|
||||
val == null ? el.removeAttributeNS(ns, k.slice(6)) : el.setAttributeNS(ns, k.slice(6), val);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase();
|
||||
el.addEventListener(ev, v);
|
||||
@@ -361,15 +360,15 @@
|
||||
onUnmount(off);
|
||||
} else if (isFunc(v)) {
|
||||
const effect = createEffect(() => {
|
||||
const val = validateAttr(k, v());
|
||||
const val2 = validateAttr(k, v());
|
||||
if (k === "class")
|
||||
el.className = val || "";
|
||||
else if (val == null)
|
||||
el.className = val2 || "";
|
||||
else if (val2 == null)
|
||||
el.removeAttribute(k);
|
||||
else if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
el[k] = val2;
|
||||
else
|
||||
el.setAttribute(k, val === true ? "" : val);
|
||||
el.setAttribute(k, val2 === true ? "" : val2);
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
@@ -379,12 +378,12 @@
|
||||
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||
}
|
||||
} else {
|
||||
const val = validateAttr(k, v);
|
||||
if (val != null) {
|
||||
const val2 = validateAttr(k, v);
|
||||
if (val2 != null) {
|
||||
if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
el[k] = val2;
|
||||
else
|
||||
el.setAttribute(k, val === true ? "" : val);
|
||||
el.setAttribute(k, val2 === true ? "" : val2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,6 +551,35 @@
|
||||
Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
Router.back = () => window.history.back();
|
||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var Anim = (show, render, { enter, leave } = {}) => {
|
||||
const wrap = Tag("div", { style: "display:contents" });
|
||||
let view = null;
|
||||
const wait = (el, cb) => {
|
||||
let done = false;
|
||||
const finish = () => !done && (done = true, cb());
|
||||
if (!el)
|
||||
return finish();
|
||||
"transitionend animationend".split(" ").map((e) => el.addEventListener(e, finish, { once: true }));
|
||||
setTimeout(finish, 500);
|
||||
};
|
||||
Watch(show, (on) => {
|
||||
if (on && !view) {
|
||||
const el = (view = Render(render)).container.firstChild;
|
||||
wrap.appendChild(view.container);
|
||||
if (enter && el) {
|
||||
el.classList.add(enter);
|
||||
el.clientTop;
|
||||
el.classList.add(enter + "-active");
|
||||
wait(el, () => el.classList.remove(enter, enter + "-active"));
|
||||
}
|
||||
} else if (!on && view) {
|
||||
const el = view.container.firstChild;
|
||||
const del = () => (view?.destroy(), view = null);
|
||||
leave && el ? (el.classList.add(leave), wait(el, del)) : del();
|
||||
}
|
||||
});
|
||||
return onUnmount(() => view?.destroy()), wrap;
|
||||
};
|
||||
var Mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!t)
|
||||
@@ -563,7 +591,7 @@
|
||||
MOUNTED_NODES.set(t, inst);
|
||||
return inst;
|
||||
};
|
||||
var SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, onMount, onUnmount, Batch });
|
||||
var SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, onMount, onUnmount, Anim, Batch });
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, SigPro);
|
||||
"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg".split(" ").forEach((t) => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c));
|
||||
|
||||
148
docs/ui/quick.md
148
docs/ui/quick.md
@@ -1,148 +0,0 @@
|
||||
# UI Components `(WIP)`
|
||||
|
||||
> **Status: Work In Progress.**
|
||||
> SigPro UI is a complete component environment built with SigPro, Tailwind CSS and DaisyUI. It provides smart components that handle their own internal logic, reactivity, and professional styling.
|
||||
|
||||
<div class="alert alert-info shadow-md my-10 border-l-8 border-info bg-info/10 text-info-content">
|
||||
<div class="flex flex-col md:flex-row items-start gap-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6 mt-1"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<div>
|
||||
<h2 class="font-black text-xl tracking-tight mb-2">📢 Official Documentation</h2>
|
||||
<p class="text-sm opacity-90 leading-relaxed mb-2">
|
||||
All official documentation, interactive examples, and usage guides are available at:
|
||||
</p>
|
||||
<div class="bg-base-100 p-3 rounded-lg inline-block">
|
||||
<a href="https://natxocc.github.io/sigpro-ui/#/" target="_blank" class="text-info font-mono text-base font-bold hover:underline">
|
||||
https://natxocc.github.io/sigpro-ui/#/
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs mt-4 opacity-70 italic">
|
||||
* Documentation is actively being updated as components are released.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 1. What are UI Components?
|
||||
|
||||
Unlike **Tag Helpers** (which are just functional mirrors of HTML tags), SigPro UI Components are smart abstractions:
|
||||
|
||||
* **Stateful**: They manage complex internal states (like date ranges, search filtering, or API lifecycles).
|
||||
* **Reactive**: Attributes prefixed with `$` are automatically tracked via `Watch`.
|
||||
* **Self-Sane**: They automatically use `._cleanups` to destroy observers or event listeners when removed from the DOM.
|
||||
* **Themed**: Fully compatible with DaisyUI v5 theme system and Tailwind v4 utility classes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
<div class="alert alert-warning shadow-md my-6 border-l-8 border-warning bg-warning/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5 mt-0.5"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">To ensure all components render correctly, your project must have:</p>
|
||||
<ul class="flex flex-wrap gap-3 mt-2">
|
||||
<li class="badge badge-warning badge-md">Tailwind CSS v4+</li>
|
||||
<li class="badge badge-warning badge-md">DaisyUI v5+</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 3. The UI Registry (Available Now)
|
||||
|
||||
| Category | Components |
|
||||
| :--- | :--- |
|
||||
| **Forms & Inputs** | `Button`, `Input`, `Select`, `Autocomplete`, `Datepicker`, `Colorpicker`, `CheckBox`, `Radio`, `Range`, `Rating`, `Swap` |
|
||||
| **Feedback** | `Alert`, `Toast`, `Modal`, `Loading`, `Badge`, `Tooltip`, `Indicator` |
|
||||
| **Navigation** | `Navbar`, `Menu`, `Drawer`, `Tabs`, `Accordion`, `Dropdown` |
|
||||
| **Data & Layout** | `Request`, `Response`, `List`, `Stack`, `Timeline`, `Stat`, `Fieldset`, `Fab` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Examples with "Superpowers"
|
||||
|
||||
### A. The Declarative API Flow (`Request` & `Response`)
|
||||
Instead of manually managing `loading` and `error` flags, use these together to handle data fetching elegantly.
|
||||
|
||||
```javascript
|
||||
// 1. Define the request (it tracks dependencies automatically)
|
||||
const userProfile = Request(
|
||||
() => `https://api.example.com/user/${userId()}`
|
||||
);
|
||||
|
||||
// 2. Render the UI based on the request state
|
||||
Div({ class: "p-4" }, [
|
||||
Response(userProfile, (data) =>
|
||||
Div([
|
||||
H1(data.name),
|
||||
P(data.email)
|
||||
])
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
### B. Smart Inputs & Autocomplete
|
||||
SigPro UI inputs handle labels, icons, password toggles, and validation states out of the box using DaisyUI v5 classes.
|
||||
|
||||
```javascript
|
||||
const searchQuery = $("");
|
||||
|
||||
Autocomplete({
|
||||
label: "Find a Country",
|
||||
placeholder: "Start typing...",
|
||||
options: ["Spain", "France", "Germany", "Italy", "Portugal"],
|
||||
$value: searchQuery,
|
||||
onSelect: (val) => console.log("Selected:", val)
|
||||
});
|
||||
```
|
||||
|
||||
### C. The Reactive Datepicker
|
||||
Handles single dates or ranges with a clean, reactive interface that automatically syncs with your signals.
|
||||
|
||||
```javascript
|
||||
const myDate = $(""); // or { start: "", end: "" } for range
|
||||
|
||||
Datepicker({
|
||||
label: "Select Expiry Date",
|
||||
$value: myDate,
|
||||
range: false
|
||||
});
|
||||
```
|
||||
|
||||
### D. Imperative Toasts & Modals
|
||||
Trigger complex UI elements from your logic. These components use `Mount` internally to ensure they are properly cleaned up from memory after they close.
|
||||
|
||||
```javascript
|
||||
// Show a notification (Self-destroying after 3s)
|
||||
Toast("Settings saved successfully!", "alert-success", 3000);
|
||||
|
||||
// Control a modal with a simple signal
|
||||
const isModalOpen = $(false);
|
||||
|
||||
Modal({
|
||||
$open: isModalOpen,
|
||||
title: "Delete Account",
|
||||
buttons: [
|
||||
Button({ class: "btn-error", onclick: doDelete }, "Confirm")
|
||||
]
|
||||
}, "This action cannot be undone.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Internationalization (i18n)
|
||||
|
||||
The UI library comes with a built-in locale system.
|
||||
|
||||
```javascript
|
||||
// Set the global UI language
|
||||
Locale("en");
|
||||
|
||||
// Access translated strings (Returns a signal that tracks the current locale)
|
||||
const t = tt("confirm");
|
||||
```
|
||||
Reference in New Issue
Block a user